From c1f02aa482c1ba1f9ab7fa55ca1e144f47ab8ccf Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:48:00 +0000 Subject: [PATCH 01/11] Add browser location bridge --- server/cmd/api/api/api.go | 8 + server/cmd/api/api/browser_location.go | 187 ++++++++++++++++++++ server/cmd/api/api/browser_location_test.go | 40 +++++ server/cmd/api/api/chromium_configure.go | 36 +++- server/cmd/api/main.go | 1 + server/cmd/chromium-launcher/main.go | 39 +++- server/cmd/chromium-launcher/main_test.go | 7 + server/lib/cdpclient/cdpclient.go | 31 ++++ server/lib/cdpclient/cdpclient_test.go | 24 +++ server/openapi.yaml | 5 + 10 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 server/cmd/api/api/browser_location.go create mode 100644 server/cmd/api/api/browser_location_test.go diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 9cde77239..891e77907 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -86,6 +86,14 @@ type ApiService struct { // or mutate its runtime flags and policies. chromiumConfigMu sync.Mutex + browserLocationMu sync.Mutex + browserLocation struct { + accepted *browserLocationBundle + applied *browserLocationBundle + lastError string + cancel context.CancelFunc + } + // inputMu serializes input-related operations (mouse, keyboard, screenshot) inputMu sync.Mutex diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go new file mode 100644 index 000000000..a59294030 --- /dev/null +++ b/server/cmd/api/api/browser_location.go @@ -0,0 +1,187 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/kernel/kernel-images/server/lib/logger" + "golang.org/x/text/language" +) + +const browserLocationApplyTimeout = 5 * time.Second + +type browserLocationBundle struct { + Epoch string `json:"epoch"` + Generation uint64 `json:"generation"` + TimeZone string `json:"timezone"` + Locale string `json:"locale"` + Languages []string `json:"languages"` +} + +type browserLocationStatus struct { + Accepted *browserLocationBundle `json:"accepted,omitempty"` + Applied *browserLocationBundle `json:"applied,omitempty"` + Error string `json:"error,omitempty"` +} + +var ( + errStaleBrowserLocation = errors.New("browser location generation is stale") + errConflictBrowserLocation = errors.New("browser location generation conflicts with accepted payload") +) + +func validateBrowserLocationBundle(raw string) (browserLocationBundle, error) { + var bundle browserLocationBundle + if err := json.Unmarshal([]byte(raw), &bundle); err != nil { + return bundle, fmt.Errorf("invalid browser_location JSON") + } + if bundle.Epoch == "" || bundle.Generation == 0 { + return bundle, fmt.Errorf("browser_location epoch and generation are required") + } + zonePath, err := browserLocationZonePath(bundle.TimeZone) + if err != nil { + return bundle, err + } + if _, err := os.Stat(zonePath); err != nil { + return bundle, fmt.Errorf("unsupported browser_location timezone") + } + locale, err := language.Parse(bundle.Locale) + if err != nil || locale.String() != bundle.Locale { + return bundle, fmt.Errorf("browser_location locale must be canonical BCP 47") + } + if len(bundle.Languages) == 0 || bundle.Languages[0] != bundle.Locale { + return bundle, fmt.Errorf("browser_location languages must start with locale") + } + for _, value := range bundle.Languages { + tag, err := language.Parse(value) + if err != nil || tag.String() != value { + return bundle, fmt.Errorf("browser_location languages must be canonical BCP 47") + } + } + return bundle, nil +} + +func browserLocationZonePath(timezone string) (string, error) { + if timezone == "" || filepath.IsAbs(timezone) || strings.Contains(timezone, "..") || strings.ContainsRune(timezone, '\x00') { + return "", fmt.Errorf("invalid browser_location timezone") + } + root := "/usr/share/zoneinfo" + path := filepath.Join(root, filepath.FromSlash(timezone)) + if !strings.HasPrefix(path, root+string(os.PathSeparator)) { + return "", fmt.Errorf("invalid browser_location timezone") + } + return path, nil +} + +func (s *ApiService) acceptBrowserLocation(bundle browserLocationBundle) error { + s.browserLocationMu.Lock() + defer s.browserLocationMu.Unlock() + current := s.browserLocation.accepted + if current != nil && current.Epoch == bundle.Epoch { + if bundle.Generation < current.Generation { + return errStaleBrowserLocation + } + if bundle.Generation == current.Generation && !browserLocationBundlesEqual(*current, bundle) { + return errConflictBrowserLocation + } + } + copy := bundle + s.browserLocation.accepted = © + s.browserLocation.lastError = "" + if s.browserLocation.cancel != nil { + s.browserLocation.cancel() + } + ctx, cancel := context.WithCancel(s.lifecycleCtx) + s.browserLocation.cancel = cancel + go s.reconcileBrowserLocation(ctx, copy) + return nil +} + +func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browserLocationBundle) { + deadline := time.Now().Add(browserLocationApplyTimeout) + var lastErr error + for { + if err := rewriteLocaltime(bundle.TimeZone); err != nil { + lastErr = err + } else { + 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 { + return err + } + observed, err := client.GetBrowserLocation(cdpCtx) + if err != nil { + return err + } + if observed.Locale != bundle.Locale || observed.AcceptLanguages != languages || observed.TimeZone != bundle.TimeZone { + return fmt.Errorf("browser location has not converged") + } + return nil + }) + } + if lastErr == nil { + s.browserLocationMu.Lock() + if accepted := s.browserLocation.accepted; accepted != nil && browserLocationBundlesEqual(*accepted, bundle) { + copy := bundle + s.browserLocation.applied = © + s.browserLocation.lastError = "" + } + s.browserLocationMu.Unlock() + return + } + if time.Now().After(deadline) { + s.browserLocationMu.Lock() + if accepted := s.browserLocation.accepted; accepted != nil && browserLocationBundlesEqual(*accepted, bundle) { + s.browserLocation.lastError = lastErr.Error() + } + s.browserLocationMu.Unlock() + logger.FromContext(ctx).Error("browser location did not converge", "error", lastErr, "generation", bundle.Generation) + return + } + select { + case <-ctx.Done(): + return + case <-time.After(50 * time.Millisecond): + } + } +} + +func rewriteLocaltime(timezone string) error { + target, err := browserLocationZonePath(timezone) + if err != nil { + return err + } + tmp := fmt.Sprintf("/etc/.localtime-kernel-%d", time.Now().UnixNano()) + if err := os.Symlink(target, tmp); err != nil { + return fmt.Errorf("stage localtime: %w", err) + } + defer os.Remove(tmp) + if err := os.Rename(tmp, "/etc/localtime"); err != nil { + return fmt.Errorf("replace localtime: %w", err) + } + return nil +} + +func (s *ApiService) browserLocationSnapshot() browserLocationStatus { + s.browserLocationMu.Lock() + defer s.browserLocationMu.Unlock() + return browserLocationStatus{Accepted: s.browserLocation.accepted, Applied: s.browserLocation.applied, Error: s.browserLocation.lastError} +} + +// GetBrowserLocationHTTP exposes accepted/applied state for internal reconciliation. +func (s *ApiService) GetBrowserLocationHTTP(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(s.browserLocationSnapshot()) +} + +func browserLocationBundlesEqual(a, b browserLocationBundle) bool { + return a.Epoch == b.Epoch && a.Generation == b.Generation && a.TimeZone == b.TimeZone && a.Locale == b.Locale && slices.Equal(a.Languages, b.Languages) +} diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go new file mode 100644 index 000000000..731bdd222 --- /dev/null +++ b/server/cmd/api/api/browser_location_test.go @@ -0,0 +1,40 @@ +package api + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateBrowserLocationBundle(t *testing.T) { + zone := filepath.Join("/usr/share/zoneinfo", "America", "New_York") + if _, err := os.Stat(zone); err != nil { + t.Skip("zoneinfo unavailable") + } + + bundle, err := validateBrowserLocationBundle(`{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-US","languages":["en-US","en"]}`) + require.NoError(t, err) + assert.Equal(t, uint64(2), bundle.Generation) + assert.Equal(t, []string{"en-US", "en"}, bundle.Languages) + + for _, raw := range []string{ + `{"epoch":"lease-1","generation":2,"timezone":"../../etc/passwd","locale":"en-US","languages":["en-US"]}`, + `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-us","languages":["en-us"]}`, + `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-US","languages":["de-DE"]}`, + } { + _, err := validateBrowserLocationBundle(raw) + assert.Error(t, err) + } +} + +func TestBrowserLocationBundlesEqual(t *testing.T) { + a := browserLocationBundle{Epoch: "lease", Generation: 1, TimeZone: "UTC", Locale: "en-US", Languages: []string{"en-US", "en"}} + b := a + b.Languages = append([]string(nil), a.Languages...) + assert.True(t, browserLocationBundlesEqual(a, b)) + b.Generation++ + assert.False(t, browserLocationBundlesEqual(a, b)) +} diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 18d43c2bd..d160ed804 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -36,7 +36,8 @@ type chromiumConfigureState struct { profileTemp string // temp archive path hasProfile bool - startURLRaw *string + startURLRaw *string + browserLocation *browserLocationBundle extItems []extensionZipItem // zipTemp paths; merged with chromiumCfgParseExtensions @@ -169,6 +170,14 @@ func (s *ApiService) chromiumConfigureLive(ctx context.Context, st *chromiumConf } } + if st.browserLocation != nil { + if err := s.acceptBrowserLocation(*st.browserLocation); err != nil { + if errors.Is(err, errStaleBrowserLocation) || errors.Is(err, errConflictBrowserLocation) { + return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: err.Error()}} + } + return cfg500ConfigureStep(chromiumConfigureStepLocation, err.Error()) + } + } chromiumConfigureNavigate(ctx, s, spec) return nil } @@ -329,6 +338,11 @@ func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumC return resp } } + if st.browserLocation != nil { + if err := s.acceptBrowserLocation(*st.browserLocation); err != nil { + return cfg500ConfigureStep(chromiumConfigureStepLocation, err.Error()) + } + } chromiumConfigureNavigate(ctx, s, spec) if len(stoppedRecordings) > 0 { go s.startNewRecordingSegments(context.WithoutCancel(ctx), stoppedRecordings) @@ -352,6 +366,7 @@ const ( chromiumConfigureStepDisplay chromiumConfigureStep = "display" chromiumConfigureStepFlags chromiumConfigureStep = "chromium_flags" chromiumConfigureStepProfile chromiumConfigureStep = "profile" + chromiumConfigureStepLocation chromiumConfigureStep = "browser_location" ) func chromiumStartURLSpec(raw *string) (startURLParsed, string) { @@ -510,6 +525,9 @@ func cfgActionables(st *chromiumConfigureState) int { if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { n++ } + if st.browserLocation != nil { + n++ + } return n } @@ -534,7 +552,7 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) err gotZip bool } var cur *pend - var gotDisplay, gotChromiumFlags, gotChromePolicies, gotStripComponents, gotProfileArchive, gotStartURL bool + var gotDisplay, gotChromiumFlags, gotChromePolicies, gotStripComponents, gotProfileArchive, gotStartURL, gotBrowserLocation bool for { part, err := mr.NextPart() @@ -611,6 +629,20 @@ func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) err } st.profileTemp = tmp.Name() st.hasProfile = true + case "browser_location": + if gotBrowserLocation { + return cfgParseBadRequest("duplicate browser_location field") + } + gotBrowserLocation = true + b, err := io.ReadAll(part) + if err != nil { + return cfgParseInternal("read browser_location field") + } + bundle, err := validateBrowserLocationBundle(string(b)) + if err != nil { + return cfgParseBadRequest(err.Error()) + } + st.browserLocation = &bundle case "start_url": if gotStartURL { return cfgParseBadRequest("duplicate start_url field") diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index eff99bcbb..8108626fa 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -221,6 +221,7 @@ func main() { // api_call event emission. Off until the telemetry handlers flip it on. r.Use(api.TelemetryHTTPMiddleware(telemetrySession.Publish)) r.Use(api.WebMCPRequestSizeMiddleware) + r.Get("/browser/location", apiService.GetBrowserLocationHTTP) // Enforce additionalProperties: false on POST /repl. r.Use(api.StrictBrowserReplBodyMiddleware) strictHandler := oapi.NewStrictHandlerWithOptions(apiService, []oapi.StrictMiddlewareFunc{ diff --git a/server/cmd/chromium-launcher/main.go b/server/cmd/chromium-launcher/main.go index d08d040b8..ed30b6306 100644 --- a/server/cmd/chromium-launcher/main.go +++ b/server/cmd/chromium-launcher/main.go @@ -35,6 +35,12 @@ func main() { runtimeFlagsPath := flag.String("runtime-flags", "/chromium/flags", "Path to runtime flags overlay file") flag.Parse() + if err := applyStartupTimezone(os.Getenv("KERNEL_BROWSER_TIMEZONE")); err != nil { + fmt.Fprintf(os.Stderr, "failed to initialize browser timezone: %v\n", err) + os.Exit(1) + } + _ = os.Unsetenv("TZ") + // Clean up stale lock file from previous SIGKILL termination // Chromium creates this lock and doesn't clean it up when killed _ = os.Remove("/home/kernel/user-data/SingletonLock") @@ -107,7 +113,7 @@ func main() { // Prepare environment. PULSE_SERVER/PULSE_SINK route chromium's audio into the // recorder's sink; the root path below relies on this inherited env, while the // non-root path re-asserts them in its runuser env allowlist. - env := os.Environ() + env := withoutEnvironmentVariable(os.Environ(), "TZ") env = append(env, "DISPLAY=:1", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket", @@ -221,3 +227,34 @@ func killExistingChromium() { // Timeout - processes may still exist but we continue anyway fmt.Fprintf(os.Stderr, "warning: chromium processes may still be running after kill attempt\n") } + +func applyStartupTimezone(timezone string) error { + timezone = strings.TrimSpace(timezone) + if timezone == "" { + return nil + } + if filepath.IsAbs(timezone) || strings.Contains(timezone, "..") || strings.ContainsRune(timezone, '\x00') { + return fmt.Errorf("invalid timezone") + } + target := filepath.Join("/usr/share/zoneinfo", filepath.FromSlash(timezone)) + if _, err := os.Stat(target); err != nil { + return fmt.Errorf("unsupported timezone: %w", err) + } + tmp := fmt.Sprintf("/etc/.localtime-kernel-%d", time.Now().UnixNano()) + if err := os.Symlink(target, tmp); err != nil { + return err + } + defer os.Remove(tmp) + return os.Rename(tmp, "/etc/localtime") +} + +func withoutEnvironmentVariable(env []string, key string) []string { + prefix := key + "=" + out := make([]string, 0, len(env)) + for _, value := range env { + if !strings.HasPrefix(value, prefix) { + out = append(out, value) + } + } + return out +} diff --git a/server/cmd/chromium-launcher/main_test.go b/server/cmd/chromium-launcher/main_test.go index 6e6d11b0c..f30355aca 100644 --- a/server/cmd/chromium-launcher/main_test.go +++ b/server/cmd/chromium-launcher/main_test.go @@ -94,3 +94,10 @@ func TestExecLookPath(t *testing.T) { t.Fatalf("execLookPath PATH search failed: p=%q err=%v", p, err) } } + +func TestWithoutEnvironmentVariable(t *testing.T) { + got := withoutEnvironmentVariable([]string{"A=1", "TZ=", "B=2"}, "TZ") + if !reflect.DeepEqual(got, []string{"A=1", "B=2"}) { + t.Fatalf("unexpected env: %v", got) + } +} diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index baeff8070..450100c09 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -816,3 +816,34 @@ func (c *Client) SetDeviceMetricsOverride(ctx context.Context, width, height int return nil } + +// BrowserLocation is Chromium's current browser-owned geography state. +type BrowserLocation struct { + Locale string `json:"locale"` + AcceptLanguages string `json:"acceptLanguages"` + TimeZone string `json:"timezone"` +} + +// SetBrowserLocation updates Chromium's session-only locale and language defaults. +func (c *Client) SetBrowserLocation(ctx context.Context, locale, acceptLanguages string) error { + _, err := c.Send(ctx, "Browser.setKernelBrowserLocation", map[string]string{ + "locale": locale, "acceptLanguages": acceptLanguages, + }, "") + if err != nil { + return fmt.Errorf("Browser.setKernelBrowserLocation: %w", err) + } + return nil +} + +// GetBrowserLocation reads Chromium's observed locale, languages and host timezone. +func (c *Client) GetBrowserLocation(ctx context.Context) (BrowserLocation, error) { + raw, err := c.Send(ctx, "Browser.getKernelBrowserLocation", nil, "") + if err != nil { + return BrowserLocation{}, fmt.Errorf("Browser.getKernelBrowserLocation: %w", err) + } + var location BrowserLocation + if err := json.Unmarshal(raw, &location); err != nil { + return BrowserLocation{}, fmt.Errorf("decode browser location: %w", err) + } + return location, nil +} diff --git a/server/lib/cdpclient/cdpclient_test.go b/server/lib/cdpclient/cdpclient_test.go index 542eaf11c..0caaf3d2a 100644 --- a/server/lib/cdpclient/cdpclient_test.go +++ b/server/lib/cdpclient/cdpclient_test.go @@ -33,6 +33,9 @@ type fakeCDP struct { getVersionCalled bool failGetVersion bool productResponse string + browserLocale string + browserLanguages string + browserTimezone string loadUnpackedCalled bool loadUnpackedPath string loadUnpackedID string @@ -135,6 +138,14 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { "jsVersion": "1.2.3", } } + case "Browser.setKernelBrowserLocation": + var params map[string]string + _ = json.Unmarshal(req.Params, ¶ms) + f.browserLocale = params["locale"] + f.browserLanguages = params["acceptLanguages"] + result = map[string]any{} + case "Browser.getKernelBrowserLocation": + result = map[string]any{"locale": f.browserLocale, "acceptLanguages": f.browserLanguages, "timezone": f.browserTimezone} case "Extensions.loadUnpacked": f.loadUnpackedCalled = true var params map[string]string @@ -825,3 +836,16 @@ func TestTargetURLs(t *testing.T) { require.NoError(t, err) assert.Equal(t, map[string]string{"tab-1": "https://example.com/"}, urls) } + +func TestBrowserLocation(t *testing.T) { + f := &fakeCDP{browserTimezone: "Europe/Berlin"} + url := startFakeCDP(t, f) + client, err := Dial(context.Background(), url) + require.NoError(t, err) + defer client.Close() + + require.NoError(t, client.SetBrowserLocation(context.Background(), "de-DE", "de-DE,de")) + location, err := client.GetBrowserLocation(context.Background()) + require.NoError(t, err) + assert.Equal(t, BrowserLocation{Locale: "de-DE", AcceptLanguages: "de-DE,de", TimeZone: "Europe/Berlin"}, location) +} diff --git a/server/openapi.yaml b/server/openapi.yaml index 94f668576..750e61cd7 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1325,6 +1325,11 @@ paths: URL text to navigate after configure. Bare hosts are normalized to https://, length is capped at 2048 bytes, and Chrome decides which schemes are navigable. type: string + browser_location: + description: >- + UTF-8 JSON browser location bundle with epoch, generation, timezone, locale, + and languages. Accepted asynchronously without restarting Chromium. + type: string extensions: type: array description: Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). From 093c083cf7f268fa9da45729bb48f6617d4bf0f6 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:55:48 +0000 Subject: [PATCH 02/11] Harden browser location lifecycle --- server/cmd/api/api/api.go | 31 +- server/cmd/api/api/browser_location.go | 348 +++++++++++++++++++- server/cmd/api/api/browser_location_test.go | 120 +++++++ server/cmd/api/api/chromium_configure.go | 36 +- server/cmd/api/main.go | 2 + server/cmd/chromium-launcher/main.go | 9 +- server/cmd/chromium-launcher/main_test.go | 16 + server/lib/cdpclient/cdpclient.go | 30 +- server/lib/cdpclient/cdpclient_test.go | 83 ++--- server/lib/devtoolsproxy/proxy.go | 31 ++ server/lib/devtoolsproxy/proxy_test.go | 78 +++++ server/lib/metrics/browser_location.go | 71 ++++ server/lib/metrics/browser_location_test.go | 23 ++ 13 files changed, 806 insertions(+), 72 deletions(-) create mode 100644 server/lib/metrics/browser_location.go create mode 100644 server/lib/metrics/browser_location_test.go diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 891e77907..03dbb0a55 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "sync" + "sync/atomic" "time" "github.com/kernel/kernel-images/server/lib/cdpmonitor" @@ -86,12 +87,26 @@ type ApiService struct { // or mutate its runtime flags and policies. chromiumConfigMu sync.Mutex - browserLocationMu sync.Mutex - browserLocation struct { - accepted *browserLocationBundle - applied *browserLocationBundle - lastError string - cancel context.CancelFunc + browserLocationMu sync.Mutex + browserLocationApplyMu sync.Mutex + browserLocationReconcile func(context.Context, browserLocationBundle) + browserLocationValidate func(context.Context, browserLocationBundle) error + browserLocation struct { + activeEpoch string + accepted *browserLocationBundle + applied *browserLocationBundle + components browserLocationComponents + lastError string + cancel context.CancelFunc + acceptedCount atomic.Uint64 + appliedCount atomic.Uint64 + retries atomic.Uint64 + stale atomic.Uint64 + conflicts atomic.Uint64 + epochRejects atomic.Uint64 + failures atomic.Uint64 + convergenceMs atomic.Uint64 + lastAttempt time.Time } // inputMu serializes input-related operations (mouse, keyboard, screenshot) @@ -213,6 +228,10 @@ func New( lifecycleCancel: cancel, } s.playwrightExecutors = newPlaywrightExecutorManager(cdpPlaywrightExecutorTabs{withCDP: s.withCDPClient}) + if err := s.initializeBrowserLocation(); err != nil { + cancel() + return nil, err + } return s, nil } diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index a59294030..961978d57 100644 --- a/server/cmd/api/api/browser_location.go +++ b/server/cmd/api/api/browser_location.go @@ -2,6 +2,7 @@ package api import ( "context" + "crypto/subtle" "encoding/json" "errors" "fmt" @@ -14,10 +15,14 @@ import ( "github.com/kernel/kernel-images/server/lib/cdpclient" "github.com/kernel/kernel-images/server/lib/logger" + "github.com/kernel/kernel-images/server/lib/metrics" "golang.org/x/text/language" ) -const browserLocationApplyTimeout = 5 * time.Second +const ( + browserLocationApplyTimeout = 5 * time.Second + defaultBrowserLocationStatePath = "/run/kernel/browser-location.json" +) type browserLocationBundle struct { Epoch string `json:"epoch"` @@ -27,15 +32,33 @@ type browserLocationBundle struct { Languages []string `json:"languages"` } +type browserLocationComponents struct { + TimeZone bool `json:"timezone"` + Browser bool `json:"browser"` + Renderers bool `json:"renderers"` + NetworkContexts bool `json:"network_contexts"` + DateTimeLocale string `json:"date_time_locale,omitempty"` + NumberLocale string `json:"number_locale,omitempty"` + CollatorLocale string `json:"collator_locale,omitempty"` +} + type browserLocationStatus struct { - Accepted *browserLocationBundle `json:"accepted,omitempty"` - Applied *browserLocationBundle `json:"applied,omitempty"` - Error string `json:"error,omitempty"` + ActiveEpoch string `json:"active_epoch,omitempty"` + Accepted *browserLocationBundle `json:"accepted,omitempty"` + Applied *browserLocationBundle `json:"applied,omitempty"` + Components browserLocationComponents `json:"components"` + Error string `json:"error,omitempty"` +} + +type browserLocationDurableState struct { + ActiveEpoch string `json:"active_epoch"` + Accepted *browserLocationBundle `json:"accepted,omitempty"` } var ( errStaleBrowserLocation = errors.New("browser location generation is stale") errConflictBrowserLocation = errors.New("browser location generation conflicts with accepted payload") + errBrowserLocationEpoch = errors.New("browser location epoch is not active") ) func validateBrowserLocationBundle(raw string) (browserLocationBundle, error) { @@ -81,37 +104,131 @@ func browserLocationZonePath(timezone string) (string, error) { return path, nil } +func (s *ApiService) validateBrowserLocationSupport(ctx context.Context, bundle browserLocationBundle) error { + var resolution cdpclient.BrowserLocationResolution + err := s.withCDPClientTimeout(ctx, time.Second, func(cdpCtx context.Context, client *cdpclient.Client) error { + var err error + resolution, err = client.ValidateBrowserLocation(cdpCtx, bundle.Locale) + return err + }) + if err != nil { + return fmt.Errorf("validate browser_location locale: %w", err) + } + observed := cdpclient.BrowserLocation{ + DateTimeLocale: resolution.DateTimeLocale, + NumberLocale: resolution.NumberLocale, + CollatorLocale: resolution.CollatorLocale, + } + if !resolvedLocalesMatch(bundle.Locale, observed) { + return fmt.Errorf("browser_location locale does not resolve consistently") + } + return nil +} + +func (s *ApiService) resetBrowserLocation(previousEpoch string, bundle browserLocationBundle) error { + if bundle.Generation != 1 { + return fmt.Errorf("browser location reset requires generation 1") + } + s.browserLocationApplyMu.Lock() + defer s.browserLocationApplyMu.Unlock() + s.browserLocationMu.Lock() + defer s.browserLocationMu.Unlock() + if s.browserLocation.activeEpoch == bundle.Epoch { + if s.browserLocation.accepted != nil && browserLocationBundlesEqual(*s.browserLocation.accepted, bundle) { + s.browserLocation.retries.Add(1) + s.startBrowserLocationReconcileLocked(bundle) + return nil + } + return errConflictBrowserLocation + } + if s.browserLocation.activeEpoch != previousEpoch { + s.browserLocation.epochRejects.Add(1) + return errBrowserLocationEpoch + } + copy := bundle + if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: bundle.Epoch, Accepted: ©}); err != nil { + return err + } + if s.browserLocation.cancel != nil { + s.browserLocation.cancel() + } + s.browserLocation.activeEpoch = bundle.Epoch + s.browserLocation.accepted = © + s.browserLocation.applied = nil + s.browserLocation.components = browserLocationComponents{} + s.browserLocation.lastError = "" + s.browserLocation.acceptedCount.Add(1) + s.startBrowserLocationReconcileLocked(bundle) + return nil +} + func (s *ApiService) acceptBrowserLocation(bundle browserLocationBundle) error { + s.browserLocationApplyMu.Lock() + defer s.browserLocationApplyMu.Unlock() s.browserLocationMu.Lock() defer s.browserLocationMu.Unlock() + if s.browserLocation.activeEpoch == "" || s.browserLocation.activeEpoch != bundle.Epoch { + s.browserLocation.epochRejects.Add(1) + return errBrowserLocationEpoch + } current := s.browserLocation.accepted - if current != nil && current.Epoch == bundle.Epoch { + if current != nil { if bundle.Generation < current.Generation { + s.browserLocation.stale.Add(1) return errStaleBrowserLocation } - if bundle.Generation == current.Generation && !browserLocationBundlesEqual(*current, bundle) { - return errConflictBrowserLocation + if bundle.Generation == current.Generation { + if !browserLocationBundlesEqual(*current, bundle) { + s.browserLocation.conflicts.Add(1) + return errConflictBrowserLocation + } + s.browserLocation.retries.Add(1) + s.startBrowserLocationReconcileLocked(bundle) + return nil } } copy := bundle + if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: s.browserLocation.activeEpoch, Accepted: ©}); err != nil { + return err + } s.browserLocation.accepted = © + s.browserLocation.applied = nil + s.browserLocation.components = browserLocationComponents{} s.browserLocation.lastError = "" + s.browserLocation.acceptedCount.Add(1) + s.startBrowserLocationReconcileLocked(bundle) + return nil +} + +func (s *ApiService) startBrowserLocationReconcileLocked(bundle browserLocationBundle) { + s.browserLocation.lastAttempt = time.Now() if s.browserLocation.cancel != nil { s.browserLocation.cancel() } ctx, cancel := context.WithCancel(s.lifecycleCtx) s.browserLocation.cancel = cancel - go s.reconcileBrowserLocation(ctx, copy) - return nil + reconcile := s.browserLocationReconcile + if reconcile == nil { + reconcile = s.reconcileBrowserLocation + } + go reconcile(ctx, bundle) } func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browserLocationBundle) { - deadline := time.Now().Add(browserLocationApplyTimeout) + started := time.Now() + deadline := started.Add(browserLocationApplyTimeout) var lastErr error for { + s.browserLocationApplyMu.Lock() + if ctx.Err() != nil || !s.browserLocationIsCurrent(bundle) { + s.browserLocationApplyMu.Unlock() + return + } + components := browserLocationComponents{} if err := rewriteLocaltime(bundle.TimeZone); err != nil { lastErr = err } else { + 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 { @@ -121,18 +238,32 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse if err != nil { return err } - if observed.Locale != bundle.Locale || observed.AcceptLanguages != languages || observed.TimeZone != bundle.TimeZone { - return fmt.Errorf("browser location has not converged") + components.Browser = observed.Locale == bundle.Locale && observed.AcceptLanguages == languages && observed.TimeZone == bundle.TimeZone + components.Renderers = observed.RenderersConverged + components.NetworkContexts = observed.NetworkContextsConverged + components.DateTimeLocale = observed.DateTimeLocale + components.NumberLocale = observed.NumberLocale + components.CollatorLocale = observed.CollatorLocale + if !components.Browser || !components.Renderers || !components.NetworkContexts || !resolvedLocalesMatch(bundle.Locale, observed) { + return fmt.Errorf("browser location components have not converged") } return nil }) } + s.browserLocationApplyMu.Unlock() + s.browserLocationMu.Lock() + if accepted := s.browserLocation.accepted; accepted != nil && browserLocationBundlesEqual(*accepted, bundle) { + s.browserLocation.components = components + } + s.browserLocationMu.Unlock() if lastErr == nil { s.browserLocationMu.Lock() if accepted := s.browserLocation.accepted; accepted != nil && browserLocationBundlesEqual(*accepted, bundle) { copy := bundle s.browserLocation.applied = © s.browserLocation.lastError = "" + s.browserLocation.appliedCount.Add(1) + s.browserLocation.convergenceMs.Store(uint64(time.Since(started).Milliseconds())) } s.browserLocationMu.Unlock() return @@ -141,11 +272,13 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse s.browserLocationMu.Lock() if accepted := s.browserLocation.accepted; accepted != nil && browserLocationBundlesEqual(*accepted, bundle) { s.browserLocation.lastError = lastErr.Error() + s.browserLocation.failures.Add(1) } s.browserLocationMu.Unlock() logger.FromContext(ctx).Error("browser location did not converge", "error", lastErr, "generation", bundle.Generation) return } + s.browserLocation.retries.Add(1) select { case <-ctx.Done(): return @@ -154,6 +287,31 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse } } +func (s *ApiService) browserLocationIsCurrent(bundle browserLocationBundle) bool { + s.browserLocationMu.Lock() + defer s.browserLocationMu.Unlock() + return s.browserLocation.accepted != nil && browserLocationBundlesEqual(*s.browserLocation.accepted, bundle) +} + +func resolvedLocalesMatch(expected string, observed cdpclient.BrowserLocation) bool { + want, err := language.Parse(expected) + 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 { + return false + } + } + return true +} + func rewriteLocaltime(timezone string) error { target, err := browserLocationZonePath(timezone) if err != nil { @@ -170,13 +328,175 @@ func rewriteLocaltime(timezone string) error { return nil } +func (s *ApiService) browserLocationStatePath() string { + if value := strings.TrimSpace(os.Getenv("KERNEL_BROWSER_LOCATION_STATE_PATH")); value != "" { + return value + } + return defaultBrowserLocationStatePath +} + +func (s *ApiService) persistBrowserLocationState(state browserLocationDurableState) error { + data, err := json.Marshal(state) + if err != nil { + return err + } + path := s.browserLocationStatePath() + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return fmt.Errorf("create browser location state directory: %w", err) + } + tmp, err := os.CreateTemp(filepath.Dir(path), ".browser-location-*") + if err != nil { + return fmt.Errorf("create browser location state: %w", err) + } + name := tmp.Name() + defer os.Remove(name) + if err := tmp.Chmod(0o600); err != nil { + tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + if err := os.Rename(name, path); err != nil { + return fmt.Errorf("replace browser location state: %w", err) + } + return nil +} + +func (s *ApiService) initializeBrowserLocation() error { + data, err := os.ReadFile(s.browserLocationStatePath()) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("read browser location state: %w", err) + } + if err == nil { + var state browserLocationDurableState + if err := json.Unmarshal(data, &state); err != nil { + return fmt.Errorf("decode browser location state: %w", err) + } + if state.Accepted != nil && state.ActiveEpoch != state.Accepted.Epoch { + return fmt.Errorf("browser location state epoch mismatch") + } + s.browserLocation.activeEpoch = state.ActiveEpoch + s.browserLocation.accepted = state.Accepted + } + if s.upstreamMgr != nil { + go s.browserLocationLifecycleLoop() + } + return nil +} + +func (s *ApiService) browserLocationLifecycleLoop() { + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + lastUpstream := "" + for { + select { + case <-s.lifecycleCtx.Done(): + return + case <-ticker.C: + upstream := s.upstreamMgr.Current() + if upstream == "" { + continue + } + s.browserLocationMu.Lock() + upstreamChanged := upstream != lastUpstream + if upstreamChanged { + lastUpstream = upstream + } + needsRetry := s.browserLocation.accepted != nil && s.browserLocation.applied == nil && s.browserLocation.lastError != "" && time.Since(s.browserLocation.lastAttempt) >= browserLocationApplyTimeout + if s.browserLocation.accepted != nil && (upstreamChanged || needsRetry) { + s.startBrowserLocationReconcileLocked(*s.browserLocation.accepted) + } + s.browserLocationMu.Unlock() + } + } +} + func (s *ApiService) browserLocationSnapshot() browserLocationStatus { s.browserLocationMu.Lock() defer s.browserLocationMu.Unlock() - return browserLocationStatus{Accepted: s.browserLocation.accepted, Applied: s.browserLocation.applied, Error: s.browserLocation.lastError} + return browserLocationStatus{ + ActiveEpoch: s.browserLocation.activeEpoch, + Accepted: s.browserLocation.accepted, + Applied: s.browserLocation.applied, + Components: s.browserLocation.components, + Error: s.browserLocation.lastError, + } +} + +func (s *ApiService) BrowserLocationMetrics() metrics.BrowserLocationSnapshot { + s.browserLocationMu.Lock() + components := s.browserLocation.components + converged := s.browserLocation.applied != nil && s.browserLocation.accepted != nil && browserLocationBundlesEqual(*s.browserLocation.applied, *s.browserLocation.accepted) + s.browserLocationMu.Unlock() + return metrics.BrowserLocationSnapshot{ + Accepted: s.browserLocation.acceptedCount.Load(), + Applied: s.browserLocation.appliedCount.Load(), + Retries: s.browserLocation.retries.Load(), + Stale: s.browserLocation.stale.Load(), + Conflicts: s.browserLocation.conflicts.Load(), + EpochRejects: s.browserLocation.epochRejects.Load(), + Failures: s.browserLocation.failures.Load(), + ConvergenceMs: s.browserLocation.convergenceMs.Load(), + Converged: converged, + TimeZoneConverged: components.TimeZone, + BrowserConverged: components.Browser, + RenderersConverged: components.Renderers, + NetworkConverged: components.NetworkContexts, + } +} + +// 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. +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 { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + defer r.Body.Close() + var reset struct { + PreviousEpoch string `json:"previous_epoch"` + Bundle browserLocationBundle `json:"bundle"` + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&reset); err != nil { + http.Error(w, "invalid browser location reset", http.StatusBadRequest) + return + } + raw, _ := json.Marshal(reset.Bundle) + validated, err := validateBrowserLocationBundle(string(raw)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + validate := s.browserLocationValidate + if validate == nil { + validate = s.validateBrowserLocationSupport + } + if err := validate(r.Context(), validated); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := s.resetBrowserLocation(reset.PreviousEpoch, validated); err != nil { + http.Error(w, err.Error(), http.StatusConflict) + return + } + w.WriteHeader(http.StatusAccepted) } -// GetBrowserLocationHTTP exposes accepted/applied state for internal reconciliation. +// GetBrowserLocationHTTP exposes accepted/applied component state for internal reconciliation. func (s *ApiService) GetBrowserLocationHTTP(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(s.browserLocationSnapshot()) diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index 731bdd222..5923f9d5f 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -1,10 +1,17 @@ package api import ( + "context" + "log/slog" + "net/http" + "net/http/httptest" "os" "path/filepath" + "strings" "testing" + "time" + "github.com/kernel/kernel-images/server/lib/devtoolsproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,3 +45,116 @@ func TestBrowserLocationBundlesEqual(t *testing.T) { b.Generation++ assert.False(t, browserLocationBundlesEqual(a, b)) } + +func newBrowserLocationStateService(t *testing.T) *ApiService { + t.Helper() + t.Setenv("KERNEL_BROWSER_LOCATION_STATE_PATH", filepath.Join(t.TempDir(), "state.json")) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + service := &ApiService{lifecycleCtx: ctx} + service.browserLocationReconcile = func(context.Context, browserLocationBundle) {} + return service +} + +func testLocationBundle(epoch string, generation uint64) browserLocationBundle { + return browserLocationBundle{Epoch: epoch, Generation: generation, TimeZone: "UTC", Locale: "en-US", Languages: []string{"en-US", "en"}} +} + +func TestBrowserLocationEpochOrdering(t *testing.T) { + service := newBrowserLocationStateService(t) + a1 := testLocationBundle("lease-a", 1) + require.NoError(t, service.resetBrowserLocation("", a1)) + + a2 := testLocationBundle("lease-a", 2) + require.NoError(t, service.acceptBrowserLocation(a2)) + assert.ErrorIs(t, service.acceptBrowserLocation(a1), errStaleBrowserLocation) + + b1 := testLocationBundle("lease-b", 1) + require.NoError(t, service.resetBrowserLocation("lease-a", b1)) + assert.ErrorIs(t, service.acceptBrowserLocation(a2), errBrowserLocationEpoch) + assert.ErrorIs(t, service.resetBrowserLocation("", a1), errBrowserLocationEpoch) + assert.Equal(t, b1, *service.browserLocationSnapshot().Accepted) + + require.NoError(t, service.acceptBrowserLocation(b1)) + conflict := b1 + conflict.Locale = "de-DE" + conflict.Languages = []string{"de-DE", "de"} + assert.ErrorIs(t, service.acceptBrowserLocation(conflict), errConflictBrowserLocation) +} + +func TestBrowserLocationStateSurvivesAPIRestart(t *testing.T) { + service := newBrowserLocationStateService(t) + bundle := testLocationBundle("lease-a", 1) + require.NoError(t, service.resetBrowserLocation("", bundle)) + + restored := &ApiService{lifecycleCtx: service.lifecycleCtx} + restored.browserLocationReconcile = func(context.Context, browserLocationBundle) {} + require.NoError(t, restored.initializeBrowserLocation()) + status := restored.browserLocationSnapshot() + assert.Equal(t, "lease-a", status.ActiveEpoch) + require.NotNil(t, status.Accepted) + assert.Equal(t, bundle, *status.Accepted) +} + +func TestBrowserLocationReconcilesAfterChromiumRestart(t *testing.T) { + service := newBrowserLocationStateService(t) + logPath := filepath.Join(t.TempDir(), "chromium.log") + require.NoError(t, os.WriteFile(logPath, nil, 0o600)) + manager := devtoolsproxy.NewUpstreamManager(logPath, slog.New(slog.DiscardHandler)) + manager.Start(service.lifecycleCtx) + service.upstreamMgr = manager + + reconciled := make(chan browserLocationBundle, 4) + service.browserLocationReconcile = func(_ context.Context, bundle browserLocationBundle) { reconciled <- bundle } + bundle := testLocationBundle("lease-a", 1) + require.NoError(t, service.resetBrowserLocation("", bundle)) + <-reconciled + go service.browserLocationLifecycleLoop() + + file, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0) + require.NoError(t, err) + defer file.Close() + _, err = file.WriteString("DevTools listening on ws://127.0.0.1:9222/devtools/browser/a\n") + require.NoError(t, err) + select { + case got := <-reconciled: + assert.Equal(t, bundle, got) + case <-time.After(3 * time.Second): + t.Fatal("location was not reconciled after Chromium became ready") + } + _, err = file.WriteString("DevTools listening on ws://127.0.0.1:9222/devtools/browser/b\n") + require.NoError(t, err) + select { + case got := <-reconciled: + assert.Equal(t, bundle, got) + case <-time.After(3 * time.Second): + t.Fatal("location was not reconciled after Chromium restart") + } +} + +func TestResetBrowserLocationHTTPRequiresInstanceIdentity(t *testing.T) { + service := newBrowserLocationStateService(t) + service.browserLocationValidate = func(context.Context, browserLocationBundle) error { return nil } + t.Setenv("KERNEL_INSTANCE_JWT", "instance-token") + body := `{"previous_epoch":"","bundle":{"epoch":"lease-a","generation":1,"timezone":"UTC","locale":"en-US","languages":["en-US","en"]}}` + + unauthorized := httptest.NewRecorder() + service.ResetBrowserLocationHTTP(unauthorized, httptest.NewRequest(http.MethodPost, "/internal/browser-location/reset", strings.NewReader(body))) + assert.Equal(t, http.StatusUnauthorized, unauthorized.Code) + assert.Empty(t, service.browserLocationSnapshot().ActiveEpoch) + + request := httptest.NewRequest(http.MethodPost, "/internal/browser-location/reset", strings.NewReader(body)) + request.Header.Set("Authorization", "Bearer instance-token") + accepted := httptest.NewRecorder() + service.ResetBrowserLocationHTTP(accepted, request) + assert.Equal(t, http.StatusAccepted, accepted.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") + err := service.resetBrowserLocation("", testLocationBundle("lease-a", 1)) + require.Error(t, err) + assert.Empty(t, service.browserLocationSnapshot().ActiveEpoch) +} diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index d160ed804..84af29297 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -170,13 +170,8 @@ func (s *ApiService) chromiumConfigureLive(ctx context.Context, st *chromiumConf } } - if st.browserLocation != nil { - if err := s.acceptBrowserLocation(*st.browserLocation); err != nil { - if errors.Is(err, errStaleBrowserLocation) || errors.Is(err, errConflictBrowserLocation) { - return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: err.Error()}} - } - return cfg500ConfigureStep(chromiumConfigureStepLocation, err.Error()) - } + if response := s.applyBrowserLocationConfig(ctx, st); response != nil { + return response } chromiumConfigureNavigate(ctx, s, spec) return nil @@ -338,10 +333,8 @@ func (s *ApiService) chromiumConfigureRestart(ctx context.Context, st *chromiumC return resp } } - if st.browserLocation != nil { - if err := s.acceptBrowserLocation(*st.browserLocation); err != nil { - return cfg500ConfigureStep(chromiumConfigureStepLocation, err.Error()) - } + if response := s.applyBrowserLocationConfig(ctx, st); response != nil { + return response } chromiumConfigureNavigate(ctx, s, spec) if len(stoppedRecordings) > 0 { @@ -356,6 +349,27 @@ type startURLParsed struct { url string } +func (s *ApiService) applyBrowserLocationConfig(ctx context.Context, st *chromiumConfigureState) oapi.ChromiumConfigureResponseObject { + if st.browserLocation == nil { + return nil + } + validate := s.browserLocationValidate + if validate == nil { + validate = s.validateBrowserLocationSupport + } + if err := validate(ctx, *st.browserLocation); err != nil { + return oapi.ChromiumConfigure400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}} + } + err := s.acceptBrowserLocation(*st.browserLocation) + if err == nil { + return nil + } + if errors.Is(err, errStaleBrowserLocation) || errors.Is(err, errConflictBrowserLocation) || errors.Is(err, errBrowserLocationEpoch) { + return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: err.Error()}} + } + return cfg500ConfigureStep(chromiumConfigureStepLocation, err.Error()) +} + type chromiumConfigureStep string const ( diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 8108626fa..2ef29e4a4 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -222,6 +222,7 @@ func main() { r.Use(api.TelemetryHTTPMiddleware(telemetrySession.Publish)) r.Use(api.WebMCPRequestSizeMiddleware) r.Get("/browser/location", apiService.GetBrowserLocationHTTP) + r.Post("/internal/browser-location/reset", apiService.ResetBrowserLocationHTTP) // Enforce additionalProperties: false on POST /repl. r.Use(api.StrictBrowserReplBodyMiddleware) strictHandler := oapi.NewStrictHandlerWithOptions(apiService, []oapi.StrictMiddlewareFunc{ @@ -348,6 +349,7 @@ func main() { rMetrics.Use(chiMiddleware.Recoverer) metricsCollectors := []metrics.Collector{ metrics.NewNetworkCollector(apiService.NetworkMetrics), + metrics.NewBrowserLocationCollector(apiService.BrowserLocationMetrics), metrics.NewChromeCollector(upstreamMgr), metrics.NewGPUCollector(), metrics.NewSystemCollector(), diff --git a/server/cmd/chromium-launcher/main.go b/server/cmd/chromium-launcher/main.go index ed30b6306..0f8ee1d21 100644 --- a/server/cmd/chromium-launcher/main.go +++ b/server/cmd/chromium-launcher/main.go @@ -35,7 +35,7 @@ func main() { runtimeFlagsPath := flag.String("runtime-flags", "/chromium/flags", "Path to runtime flags overlay file") flag.Parse() - if err := applyStartupTimezone(os.Getenv("KERNEL_BROWSER_TIMEZONE")); err != nil { + if err := applyStartupTimezone(startupTimezone(os.Getenv)); err != nil { fmt.Fprintf(os.Stderr, "failed to initialize browser timezone: %v\n", err) os.Exit(1) } @@ -258,3 +258,10 @@ func withoutEnvironmentVariable(env []string, key string) []string { } return out } + +func startupTimezone(getenv func(string) string) string { + if timezone := getenv("KERNEL_BROWSER_TIMEZONE"); timezone != "" { + return timezone + } + return getenv("TZ") +} diff --git a/server/cmd/chromium-launcher/main_test.go b/server/cmd/chromium-launcher/main_test.go index f30355aca..dd5ec735d 100644 --- a/server/cmd/chromium-launcher/main_test.go +++ b/server/cmd/chromium-launcher/main_test.go @@ -101,3 +101,19 @@ func TestWithoutEnvironmentVariable(t *testing.T) { t.Fatalf("unexpected env: %v", got) } } + +func TestStartupTimezone(t *testing.T) { + values := map[string]string{"TZ": "America/Chicago", "KERNEL_BROWSER_TIMEZONE": "Europe/Berlin"} + getenv := func(key string) string { return values[key] } + if got := startupTimezone(getenv); got != "Europe/Berlin" { + t.Fatalf("unexpected timezone: %s", got) + } + delete(values, "KERNEL_BROWSER_TIMEZONE") + if got := startupTimezone(getenv); got != "America/Chicago" { + t.Fatalf("unexpected fallback timezone: %s", got) + } + delete(values, "TZ") + if got := startupTimezone(getenv); got != "" { + t.Fatalf("unexpected empty timezone: %s", got) + } +} diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index 450100c09..6dc2c5c8e 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -819,9 +819,33 @@ func (c *Client) SetDeviceMetricsOverride(ctx context.Context, width, height int // BrowserLocation is Chromium's current browser-owned geography state. type BrowserLocation struct { - Locale string `json:"locale"` - AcceptLanguages string `json:"acceptLanguages"` - TimeZone string `json:"timezone"` + Locale string `json:"locale"` + AcceptLanguages string `json:"acceptLanguages"` + TimeZone string `json:"timezone"` + DateTimeLocale string `json:"dateTimeLocale"` + NumberLocale string `json:"numberLocale"` + CollatorLocale string `json:"collatorLocale"` + RenderersConverged bool `json:"renderersConverged"` + NetworkContextsConverged bool `json:"networkContextsConverged"` +} + +type BrowserLocationResolution struct { + DateTimeLocale string `json:"dateTimeLocale"` + NumberLocale string `json:"numberLocale"` + CollatorLocale string `json:"collatorLocale"` +} + +// ValidateBrowserLocation verifies locale behavior against Chromium's shipped ICU data. +func (c *Client) ValidateBrowserLocation(ctx context.Context, locale string) (BrowserLocationResolution, error) { + raw, err := c.Send(ctx, "Browser.validateKernelBrowserLocation", map[string]string{"locale": locale}, "") + if err != nil { + return BrowserLocationResolution{}, fmt.Errorf("Browser.validateKernelBrowserLocation: %w", err) + } + var resolution BrowserLocationResolution + if err := json.Unmarshal(raw, &resolution); err != nil { + return BrowserLocationResolution{}, fmt.Errorf("decode browser location validation: %w", err) + } + return resolution, nil } // SetBrowserLocation updates Chromium's session-only locale and language defaults. diff --git a/server/lib/cdpclient/cdpclient_test.go b/server/lib/cdpclient/cdpclient_test.go index 0caaf3d2a..8d497c4d3 100644 --- a/server/lib/cdpclient/cdpclient_test.go +++ b/server/lib/cdpclient/cdpclient_test.go @@ -18,40 +18,43 @@ import ( // fakeCDP is a minimal CDP server that responds to the commands used by // SetDeviceMetricsOverride and GetBrowserVersion. type fakeCDP struct { - getTargetsCalls int - attachCalled bool - setMetricsCalled bool - setMetricsWidth int - setMetricsHeight int - detachCalled bool - pageTargetID string - sessionID string - failGetTargets bool - failSetMetrics bool - returnNoPageTargets bool - noPageTargetsFor int - getVersionCalled bool - failGetVersion bool - productResponse string - browserLocale string - browserLanguages string - browserTimezone string - loadUnpackedCalled bool - loadUnpackedPath string - loadUnpackedID string - failLoadUnpacked bool - getExtensionsCalled bool - extensions []ExtensionInfo - failGetExtensions bool - navigateCalled bool - navigateCalls int - navigateURL string - pageStates []string - pageStateIndex int - createdTargetID string - createParams map[string]any - closedTargetID string - closeTargetMissing bool + getTargetsCalls int + attachCalled bool + setMetricsCalled bool + setMetricsWidth int + setMetricsHeight int + detachCalled bool + pageTargetID string + sessionID string + failGetTargets bool + failSetMetrics bool + returnNoPageTargets bool + noPageTargetsFor int + getVersionCalled bool + failGetVersion bool + productResponse string + browserLocale string + browserLanguages string + browserTimezone string + browserDateTimeLocale string + browserNumberLocale string + browserCollatorLocale string + loadUnpackedCalled bool + loadUnpackedPath string + loadUnpackedID string + failLoadUnpacked bool + getExtensionsCalled bool + extensions []ExtensionInfo + failGetExtensions bool + navigateCalled bool + navigateCalls int + navigateURL string + pageStates []string + pageStateIndex int + createdTargetID string + createParams map[string]any + closedTargetID string + closeTargetMissing bool } func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { @@ -138,6 +141,8 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { "jsVersion": "1.2.3", } } + case "Browser.validateKernelBrowserLocation": + result = map[string]any{"dateTimeLocale": f.browserDateTimeLocale, "numberLocale": f.browserNumberLocale, "collatorLocale": f.browserCollatorLocale} case "Browser.setKernelBrowserLocation": var params map[string]string _ = json.Unmarshal(req.Params, ¶ms) @@ -145,7 +150,7 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { f.browserLanguages = params["acceptLanguages"] result = map[string]any{} case "Browser.getKernelBrowserLocation": - result = map[string]any{"locale": f.browserLocale, "acceptLanguages": f.browserLanguages, "timezone": f.browserTimezone} + 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} case "Extensions.loadUnpacked": f.loadUnpackedCalled = true var params map[string]string @@ -838,14 +843,18 @@ func TestTargetURLs(t *testing.T) { } func TestBrowserLocation(t *testing.T) { - f := &fakeCDP{browserTimezone: "Europe/Berlin"} + f := &fakeCDP{browserTimezone: "Europe/Berlin", browserDateTimeLocale: "de", browserNumberLocale: "de", browserCollatorLocale: "de"} url := startFakeCDP(t, f) client, err := Dial(context.Background(), url) require.NoError(t, err) defer client.Close() + resolution, err := client.ValidateBrowserLocation(context.Background(), "de-DE") + 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")) location, err := client.GetBrowserLocation(context.Background()) require.NoError(t, err) - assert.Equal(t, BrowserLocation{Locale: "de-DE", AcceptLanguages: "de-DE,de", TimeZone: "Europe/Berlin"}, location) + 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) } diff --git a/server/lib/devtoolsproxy/proxy.go b/server/lib/devtoolsproxy/proxy.go index 0ba13e961..923a3759d 100644 --- a/server/lib/devtoolsproxy/proxy.go +++ b/server/lib/devtoolsproxy/proxy.go @@ -27,6 +27,34 @@ import ( "github.com/nrednav/cuid2" ) +var internalCDPMethods = map[string]struct{}{ + "Browser.validateKernelBrowserLocation": {}, + "Browser.setKernelBrowserLocation": {}, + "Browser.getKernelBrowserLocation": {}, +} + +func filterInternalCDPMethod(message []byte) []byte { + var command struct { + Method string `json:"method"` + } + if json.Unmarshal(message, &command) != nil { + return message + } + if _, blocked := internalCDPMethods[command.Method]; !blocked { + return message + } + var fields map[string]json.RawMessage + if json.Unmarshal(message, &fields) != nil { + return message + } + fields["method"] = json.RawMessage(`"Kernel.internalMethodUnavailable"`) + filtered, err := json.Marshal(fields) + if err != nil { + return message + } + return filtered +} + var devtoolsListeningRegexp = regexp.MustCompile(`DevTools listening on (ws://\S+)`) // UpstreamManager tails the Chromium supervisord log and extracts the current DevTools @@ -328,6 +356,9 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess logCDPMessage(logger, direction, mt, msg) } msgCount.Add(1) + if direction == "->" && mt == websocket.MessageText { + return filterInternalCDPMethod(msg) + } return msg } diff --git a/server/lib/devtoolsproxy/proxy_test.go b/server/lib/devtoolsproxy/proxy_test.go index f616531f4..9733c2261 100644 --- a/server/lib/devtoolsproxy/proxy_test.go +++ b/server/lib/devtoolsproxy/proxy_test.go @@ -866,3 +866,81 @@ func TestWebSocketProxyHandler_EmitsUpstreamErrorOnDialFailure(t *testing.T) { // controlOn is the gate a proxy test needs to see cdp_command events at all. func controlOn() bool { return true } + +func TestFilterInternalCDPMethod(t *testing.T) { + for _, method := range []string{"Browser.validateKernelBrowserLocation", "Browser.setKernelBrowserLocation", "Browser.getKernelBrowserLocation"} { + input := []byte(`{"id":7,"method":"` + method + `","params":{"locale":"de-DE"}}`) + var got map[string]any + if err := json.Unmarshal(filterInternalCDPMethod(input), &got); err != nil { + t.Fatal(err) + } + if got["method"] != "Kernel.internalMethodUnavailable" || got["id"] != float64(7) { + t.Fatalf("unexpected filtered command: %#v", got) + } + } + + emulation := []byte(`{"id":8,"method":"Emulation.setLocaleOverride","params":{"locale":"de-DE"}}`) + if got := filterInternalCDPMethod(emulation); string(got) != string(emulation) { + t.Fatalf("standard command changed: %s", got) + } +} + +func TestWebSocketProxyRejectsInternalLocationMethods(t *testing.T) { + methods := make(chan string, 2) + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}}) + if err != nil { + return + } + defer conn.Close(websocket.StatusNormalClosure, "") + for { + _, message, err := conn.Read(r.Context()) + if err != nil { + return + } + var command struct { + ID int `json:"id"` + Method string `json:"method"` + } + if json.Unmarshal(message, &command) != nil { + return + } + methods <- command.Method + response, _ := json.Marshal(map[string]any{"id": command.ID, "result": map[string]any{}}) + if conn.Write(r.Context(), websocket.MessageText, response) != nil { + return + } + } + })) + defer upstream.Close() + + upstreamURL, _ := url.Parse(upstream.URL) + manager := NewUpstreamManager("/dev/null", silentLogger()) + manager.setCurrent("ws://" + upstreamURL.Host) + proxy := httptest.NewServer(WebSocketProxyHandler(manager, silentLogger(), false, scaletozero.NewNoopController(), nil, nil, nil, nil)) + defer proxy.Close() + proxyURL, _ := url.Parse(proxy.URL) + proxyURL.Scheme = "ws" + conn, _, err := websocket.Dial(context.Background(), proxyURL.String(), nil) + if err != nil { + t.Fatal(err) + } + defer conn.Close(websocket.StatusNormalClosure, "") + + commands := []string{"Browser.setKernelBrowserLocation", "Emulation.setLocaleOverride"} + for index, method := range commands { + message, _ := json.Marshal(map[string]any{"id": index + 1, "method": method}) + if err := conn.Write(context.Background(), websocket.MessageText, message); err != nil { + t.Fatal(err) + } + if _, _, err := conn.Read(context.Background()); err != nil { + t.Fatal(err) + } + } + if got := <-methods; got != "Kernel.internalMethodUnavailable" { + t.Fatalf("internal method reached upstream as %q", got) + } + if got := <-methods; got != "Emulation.setLocaleOverride" { + t.Fatalf("standard Emulation method changed to %q", got) + } +} diff --git a/server/lib/metrics/browser_location.go b/server/lib/metrics/browser_location.go new file mode 100644 index 000000000..ce1106883 --- /dev/null +++ b/server/lib/metrics/browser_location.go @@ -0,0 +1,71 @@ +package metrics + +import "context" + +// BrowserLocationSnapshot contains process-lifetime control-plane counters and +// current component convergence. It intentionally carries no lease or locale +// labels so metric cardinality remains bounded. +type BrowserLocationSnapshot struct { + Accepted uint64 + Applied uint64 + Retries uint64 + Stale uint64 + Conflicts uint64 + EpochRejects uint64 + Failures uint64 + ConvergenceMs uint64 + Converged bool + TimeZoneConverged bool + BrowserConverged bool + RenderersConverged bool + NetworkConverged bool +} + +type BrowserLocationCollector struct { + snapshot func() BrowserLocationSnapshot +} + +func NewBrowserLocationCollector(snapshot func() BrowserLocationSnapshot) *BrowserLocationCollector { + return &BrowserLocationCollector{snapshot: snapshot} +} + +func (*BrowserLocationCollector) Name() string { return "browser_location" } + +func (c *BrowserLocationCollector) Collect(_ context.Context, w *Writer) error { + s := c.snapshot() + counters := []struct { + name string + help string + value uint64 + }{ + {"kernel_browser_location_accepted_total", "Browser location bundles accepted.", s.Accepted}, + {"kernel_browser_location_applied_total", "Browser location bundles observed fully converged.", s.Applied}, + {"kernel_browser_location_retries_total", "Browser location same-value and convergence retries.", s.Retries}, + {"kernel_browser_location_stale_total", "Stale browser location generations rejected.", s.Stale}, + {"kernel_browser_location_conflicts_total", "Equal-generation browser location payload conflicts rejected.", s.Conflicts}, + {"kernel_browser_location_epoch_rejects_total", "Browser location bundles rejected for a non-active epoch.", s.EpochRejects}, + {"kernel_browser_location_failures_total", "Browser location reconciliation attempts that exhausted their deadline.", s.Failures}, + } + for _, counter := range counters { + w.Metric(counter.name, counter.help, "counter") + w.Sample(counter.name, nil, float64(counter.value)) + } + w.Metric("kernel_browser_location_convergence_milliseconds", "Most recent browser location convergence latency.", "gauge") + w.Sample("kernel_browser_location_convergence_milliseconds", nil, float64(s.ConvergenceMs)) + w.Metric("kernel_browser_location_converged", "Whether the accepted browser location generation is fully converged.", "gauge") + w.Sample("kernel_browser_location_converged", nil, boolToFloat(s.Converged)) + components := []struct { + name string + value bool + }{ + {"timezone", s.TimeZoneConverged}, + {"browser", s.BrowserConverged}, + {"renderers", s.RenderersConverged}, + {"network_contexts", s.NetworkConverged}, + } + w.Metric("kernel_browser_location_component_converged", "Whether each browser location component has converged.", "gauge") + for _, component := range components { + w.Sample("kernel_browser_location_component_converged", []Label{{Name: "component", Value: component.name}}, boolToFloat(component.value)) + } + return nil +} diff --git a/server/lib/metrics/browser_location_test.go b/server/lib/metrics/browser_location_test.go new file mode 100644 index 000000000..48f110f9e --- /dev/null +++ b/server/lib/metrics/browser_location_test.go @@ -0,0 +1,23 @@ +package metrics + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBrowserLocationCollector(t *testing.T) { + collector := NewBrowserLocationCollector(func() BrowserLocationSnapshot { + return BrowserLocationSnapshot{Accepted: 2, Applied: 1, Retries: 3, Converged: false, TimeZoneConverged: true} + }) + writer := &Writer{} + require.NoError(t, collector.Collect(context.Background(), writer)) + out := string(writer.Bytes()) + assert.Contains(t, out, "kernel_browser_location_accepted_total 2") + assert.Contains(t, out, "kernel_browser_location_applied_total 1") + assert.Contains(t, out, "kernel_browser_location_retries_total 3") + assert.Contains(t, out, `kernel_browser_location_component_converged{component="timezone"} 1`) + assert.Contains(t, out, `kernel_browser_location_component_converged{component="renderers"} 0`) +} From 818b3fc1e6fe46c0c0c50eec9d80ee6b41474c14 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:02:33 +0000 Subject: [PATCH 03/11] Register location routes after middleware --- server/cmd/api/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 2ef29e4a4..67cbfb3af 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -221,10 +221,10 @@ func main() { // api_call event emission. Off until the telemetry handlers flip it on. r.Use(api.TelemetryHTTPMiddleware(telemetrySession.Publish)) r.Use(api.WebMCPRequestSizeMiddleware) - r.Get("/browser/location", apiService.GetBrowserLocationHTTP) - r.Post("/internal/browser-location/reset", apiService.ResetBrowserLocationHTTP) // Enforce additionalProperties: false on POST /repl. r.Use(api.StrictBrowserReplBodyMiddleware) + r.Get("/browser/location", apiService.GetBrowserLocationHTTP) + r.Post("/internal/browser-location/reset", apiService.ResetBrowserLocationHTTP) strictHandler := oapi.NewStrictHandlerWithOptions(apiService, []oapi.StrictMiddlewareFunc{ api.TelemetryStrictMiddleware(), }, oapi.StrictHTTPServerOptions{ From c62f1141531b02e5adbdecf346129cf1f5c303a9 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:44:42 +0000 Subject: [PATCH 04/11] Authorize browser location lease resets --- server/cmd/api/api/browser_location.go | 11 ++++++++--- server/cmd/api/api/browser_location_test.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index 961978d57..9f5a317a1 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 { @@ -455,12 +457,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 5923f9d5f..1a99b9632 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -151,6 +151,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") From a584783685e5ef9696ee8437790c55cfe80fe1a1 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 18 Sep 2026 09:21:58 +0000 Subject: [PATCH 05/11] Require regional location convergence --- server/cmd/api/api/browser_location.go | 9 ++------- server/cmd/api/api/browser_location_test.go | 14 ++++++++++++++ server/lib/cdpclient/cdpclient.go | 4 ++-- server/lib/cdpclient/cdpclient_test.go | 3 ++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index 9f5a317a1..be3c9a82f 100644 --- a/server/cmd/api/api/browser_location.go +++ b/server/cmd/api/api/browser_location.go @@ -233,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) @@ -300,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 } } diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index 1a99b9632..5054f8045 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 diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index 6dc2c5c8e..526c19347 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -849,9 +849,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 8d497c4d3..fa04ca416 100644 --- a/server/lib/cdpclient/cdpclient_test.go +++ b/server/lib/cdpclient/cdpclient_test.go @@ -148,6 +148,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} @@ -853,7 +854,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) From 9755de806c2f79d135e96fc7e853b6af5a84adcc Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:28:44 +0000 Subject: [PATCH 06/11] Adapt location proxy test to drain registry --- server/lib/devtoolsproxy/proxy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/devtoolsproxy/proxy_test.go b/server/lib/devtoolsproxy/proxy_test.go index 9733c2261..d8e00182e 100644 --- a/server/lib/devtoolsproxy/proxy_test.go +++ b/server/lib/devtoolsproxy/proxy_test.go @@ -917,7 +917,7 @@ func TestWebSocketProxyRejectsInternalLocationMethods(t *testing.T) { upstreamURL, _ := url.Parse(upstream.URL) manager := NewUpstreamManager("/dev/null", silentLogger()) manager.setCurrent("ws://" + upstreamURL.Host) - proxy := httptest.NewServer(WebSocketProxyHandler(manager, silentLogger(), false, scaletozero.NewNoopController(), nil, nil, nil, nil)) + proxy := httptest.NewServer(WebSocketProxyHandler(manager, silentLogger(), false, scaletozero.NewNoopController(), nil, nil, nil, nil, nil)) defer proxy.Close() proxyURL, _ := url.Parse(proxy.URL) proxyURL.Scheme = "ws" From 76153098d1cf794742e736a0b766ae12ab35ddc2 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:37:12 +0000 Subject: [PATCH 07/11] Require generation acknowledgements and generated capabilities for browser location Send Chromium a monotonic browser generation per accepted bundle and mark a bundle applied only after Chromium reports renderer, worker, host-timezone and network-context acknowledgements for that generation. A Chromium restart clears applied state until the new process acknowledges. Explicit locales and timezones are validated against a capability manifest generated from the Chromium 154 browser in the image instead of ICU component-locale readback. --- server/cmd/api/api/api.go | 32 +- server/cmd/api/api/browser_location.go | 133 +-- server/cmd/api/api/browser_location_test.go | 60 +- server/cmd/api/api/chromium_configure.go | 7 - server/lib/browserlocation/capabilities.go | 78 ++ server/lib/browserlocation/capabilities.json | 1019 +++++++++++++++++ .../lib/browserlocation/capabilities_test.go | 38 + server/lib/cdpclient/cdpclient.go | 39 +- server/lib/cdpclient/cdpclient_test.go | 100 +- 9 files changed, 1312 insertions(+), 194 deletions(-) create mode 100644 server/lib/browserlocation/capabilities.go create mode 100644 server/lib/browserlocation/capabilities.json create mode 100644 server/lib/browserlocation/capabilities_test.go diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index 03dbb0a55..4c4dbeafe 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -90,23 +90,23 @@ type ApiService struct { browserLocationMu sync.Mutex browserLocationApplyMu sync.Mutex browserLocationReconcile func(context.Context, browserLocationBundle) - browserLocationValidate func(context.Context, browserLocationBundle) error browserLocation struct { - activeEpoch string - accepted *browserLocationBundle - applied *browserLocationBundle - components browserLocationComponents - lastError string - cancel context.CancelFunc - acceptedCount atomic.Uint64 - appliedCount atomic.Uint64 - retries atomic.Uint64 - stale atomic.Uint64 - conflicts atomic.Uint64 - epochRejects atomic.Uint64 - failures atomic.Uint64 - convergenceMs atomic.Uint64 - lastAttempt time.Time + activeEpoch string + browserGeneration uint64 + accepted *browserLocationBundle + applied *browserLocationBundle + components browserLocationComponents + lastError string + cancel context.CancelFunc + acceptedCount atomic.Uint64 + appliedCount atomic.Uint64 + retries atomic.Uint64 + stale atomic.Uint64 + conflicts atomic.Uint64 + epochRejects atomic.Uint64 + failures atomic.Uint64 + convergenceMs atomic.Uint64 + lastAttempt time.Time } // inputMu serializes input-related operations (mouse, keyboard, screenshot) diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index be3c9a82f..13dec28de 100644 --- a/server/cmd/api/api/browser_location.go +++ b/server/cmd/api/api/browser_location.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/kernel/kernel-images/server/lib/browserlocation" "github.com/kernel/kernel-images/server/lib/cdpclient" "github.com/kernel/kernel-images/server/lib/logger" "github.com/kernel/kernel-images/server/lib/metrics" @@ -34,27 +35,34 @@ type browserLocationBundle struct { Languages []string `json:"languages"` } +// browserLocationComponents records the acknowledgements Chromium returned for +// the accepted bundle's browser generation. type browserLocationComponents struct { - TimeZone bool `json:"timezone"` - Browser bool `json:"browser"` - Renderers bool `json:"renderers"` - NetworkContexts bool `json:"network_contexts"` - DateTimeLocale string `json:"date_time_locale,omitempty"` - NumberLocale string `json:"number_locale,omitempty"` - CollatorLocale string `json:"collator_locale,omitempty"` + // TimeZone: /etc/localtime was replaced and every renderer observed it. + TimeZone bool `json:"timezone"` + // Browser: Chromium holds this generation's locale and languages. + Browser bool `json:"browser"` + // Renderers: every renderer applied them to its pages and existing workers. + Renderers bool `json:"renderers"` + NetworkContexts bool `json:"network_contexts"` } type browserLocationStatus struct { - ActiveEpoch string `json:"active_epoch,omitempty"` - Accepted *browserLocationBundle `json:"accepted,omitempty"` - Applied *browserLocationBundle `json:"applied,omitempty"` - Components browserLocationComponents `json:"components"` - Error string `json:"error,omitempty"` + ActiveEpoch string `json:"active_epoch,omitempty"` + Accepted *browserLocationBundle `json:"accepted,omitempty"` + Applied *browserLocationBundle `json:"applied,omitempty"` + Components browserLocationComponents `json:"components"` + Error string `json:"error,omitempty"` + CapabilitiesVersion string `json:"capabilities_version"` } type browserLocationDurableState struct { ActiveEpoch string `json:"active_epoch"` Accepted *browserLocationBundle `json:"accepted,omitempty"` + // BrowserGeneration is the monotonic generation sent to Chromium for + // Accepted. Bundle generations restart with every lease epoch, but + // Chromium's must keep increasing for the life of the process. + BrowserGeneration uint64 `json:"browser_generation,omitempty"` } var ( @@ -71,6 +79,10 @@ func validateBrowserLocationBundle(raw string) (browserLocationBundle, error) { if bundle.Epoch == "" || bundle.Generation == 0 { return bundle, fmt.Errorf("browser_location epoch and generation are required") } + capabilities := browserlocation.Current() + if !capabilities.SupportsTimeZone(bundle.TimeZone) { + return bundle, fmt.Errorf("unsupported browser_location timezone") + } zonePath, err := browserLocationZonePath(bundle.TimeZone) if err != nil { return bundle, err @@ -78,9 +90,8 @@ func validateBrowserLocationBundle(raw string) (browserLocationBundle, error) { if _, err := os.Stat(zonePath); err != nil { return bundle, fmt.Errorf("unsupported browser_location timezone") } - locale, err := language.Parse(bundle.Locale) - if err != nil || locale.String() != bundle.Locale { - return bundle, fmt.Errorf("browser_location locale must be canonical BCP 47") + if !capabilities.SupportsLocale(bundle.Locale) { + return bundle, fmt.Errorf("unsupported browser_location locale") } if len(bundle.Languages) == 0 || bundle.Languages[0] != bundle.Locale { return bundle, fmt.Errorf("browser_location languages must start with locale") @@ -106,27 +117,6 @@ func browserLocationZonePath(timezone string) (string, error) { return path, nil } -func (s *ApiService) validateBrowserLocationSupport(ctx context.Context, bundle browserLocationBundle) error { - var resolution cdpclient.BrowserLocationResolution - err := s.withCDPClientTimeout(ctx, time.Second, func(cdpCtx context.Context, client *cdpclient.Client) error { - var err error - resolution, err = client.ValidateBrowserLocation(cdpCtx, bundle.Locale) - return err - }) - if err != nil { - return fmt.Errorf("validate browser_location locale: %w", err) - } - observed := cdpclient.BrowserLocation{ - DateTimeLocale: resolution.DateTimeLocale, - NumberLocale: resolution.NumberLocale, - CollatorLocale: resolution.CollatorLocale, - } - if !resolvedLocalesMatch(bundle.Locale, observed) { - return fmt.Errorf("browser_location locale does not resolve consistently") - } - return nil -} - func (s *ApiService) resetBrowserLocation(previousEpoch string, bundle browserLocationBundle) error { if bundle.Generation != 1 { return fmt.Errorf("browser location reset requires generation 1") @@ -148,13 +138,15 @@ func (s *ApiService) resetBrowserLocation(previousEpoch string, bundle browserLo return errBrowserLocationEpoch } copy := bundle - if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: bundle.Epoch, Accepted: ©}); err != nil { + browserGeneration := s.browserLocation.browserGeneration + 1 + if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: bundle.Epoch, Accepted: ©, BrowserGeneration: browserGeneration}); err != nil { return err } if s.browserLocation.cancel != nil { s.browserLocation.cancel() } s.browserLocation.activeEpoch = bundle.Epoch + s.browserLocation.browserGeneration = browserGeneration s.browserLocation.accepted = © s.browserLocation.applied = nil s.browserLocation.components = browserLocationComponents{} @@ -190,9 +182,11 @@ func (s *ApiService) acceptBrowserLocation(bundle browserLocationBundle) error { } } copy := bundle - if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: s.browserLocation.activeEpoch, Accepted: ©}); err != nil { + browserGeneration := s.browserLocation.browserGeneration + 1 + if err := s.persistBrowserLocationState(browserLocationDurableState{ActiveEpoch: s.browserLocation.activeEpoch, Accepted: ©, BrowserGeneration: browserGeneration}); err != nil { return err } + s.browserLocation.browserGeneration = browserGeneration s.browserLocation.accepted = © s.browserLocation.applied = nil s.browserLocation.components = browserLocationComponents{} @@ -222,7 +216,8 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse var lastErr error for { s.browserLocationApplyMu.Lock() - if ctx.Err() != nil || !s.browserLocationIsCurrent(bundle) { + browserGeneration, current := s.browserLocationGeneration(bundle) + if ctx.Err() != nil || !current { s.browserLocationApplyMu.Unlock() return } @@ -230,23 +225,23 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse if err := rewriteLocaltime(bundle.TimeZone); err != nil { lastErr = err } else { - 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, bundle.TimeZone); err != nil { + if err := client.SetBrowserLocation(cdpCtx, browserGeneration, bundle.Locale, languages, bundle.TimeZone); err != nil { return err } observed, err := client.GetBrowserLocation(cdpCtx) if err != nil { return err } - components.Browser = observed.Locale == bundle.Locale && observed.AcceptLanguages == languages && observed.TimeZone == bundle.TimeZone + if observed.Generation != browserGeneration { + return fmt.Errorf("browser location generation %d is not current", browserGeneration) + } + components.Browser = observed.Locale == bundle.Locale && observed.AcceptLanguages == languages + components.TimeZone = observed.TimeZone == bundle.TimeZone && observed.TimeZoneConverged components.Renderers = observed.RenderersConverged components.NetworkContexts = observed.NetworkContextsConverged - components.DateTimeLocale = observed.DateTimeLocale - components.NumberLocale = observed.NumberLocale - components.CollatorLocale = observed.CollatorLocale - if !components.Browser || !components.Renderers || !components.NetworkContexts || !resolvedLocalesMatch(bundle.Locale, observed) { + if !components.Browser || !components.TimeZone || !components.Renderers || !components.NetworkContexts { return fmt.Errorf("browser location components have not converged") } return nil @@ -289,24 +284,13 @@ func (s *ApiService) reconcileBrowserLocation(ctx context.Context, bundle browse } } -func (s *ApiService) browserLocationIsCurrent(bundle browserLocationBundle) bool { +// browserLocationGeneration returns the browser generation assigned to bundle +// and whether bundle is still the accepted one. +func (s *ApiService) browserLocationGeneration(bundle browserLocationBundle) (uint64, bool) { s.browserLocationMu.Lock() defer s.browserLocationMu.Unlock() - return s.browserLocation.accepted != nil && browserLocationBundlesEqual(*s.browserLocation.accepted, bundle) -} - -func resolvedLocalesMatch(expected string, observed cdpclient.BrowserLocation) bool { - want, err := language.Parse(expected) - if err != nil { - return false - } - for _, value := range []string{observed.DateTimeLocale, observed.NumberLocale, observed.CollatorLocale} { - tag, err := language.Parse(value) - if err != nil || tag.String() != want.String() { - return false - } - } - return true + current := s.browserLocation.accepted != nil && browserLocationBundlesEqual(*s.browserLocation.accepted, bundle) + return s.browserLocation.browserGeneration, current } func rewriteLocaltime(timezone string) error { @@ -383,6 +367,7 @@ func (s *ApiService) initializeBrowserLocation() error { } s.browserLocation.activeEpoch = state.ActiveEpoch s.browserLocation.accepted = state.Accepted + s.browserLocation.browserGeneration = state.BrowserGeneration } if s.upstreamMgr != nil { go s.browserLocationLifecycleLoop() @@ -408,6 +393,11 @@ func (s *ApiService) browserLocationLifecycleLoop() { if upstreamChanged { lastUpstream = upstream } + if upstreamChanged { + // A new Chromium process has not acknowledged anything yet. + s.browserLocation.applied = nil + s.browserLocation.components = browserLocationComponents{} + } needsRetry := s.browserLocation.accepted != nil && s.browserLocation.applied == nil && s.browserLocation.lastError != "" && time.Since(s.browserLocation.lastAttempt) >= browserLocationApplyTimeout if s.browserLocation.accepted != nil && (upstreamChanged || needsRetry) { s.startBrowserLocationReconcileLocked(*s.browserLocation.accepted) @@ -421,11 +411,12 @@ func (s *ApiService) browserLocationSnapshot() browserLocationStatus { s.browserLocationMu.Lock() defer s.browserLocationMu.Unlock() return browserLocationStatus{ - ActiveEpoch: s.browserLocation.activeEpoch, - Accepted: s.browserLocation.accepted, - Applied: s.browserLocation.applied, - Components: s.browserLocation.components, - Error: s.browserLocation.lastError, + ActiveEpoch: s.browserLocation.activeEpoch, + Accepted: s.browserLocation.accepted, + Applied: s.browserLocation.applied, + Components: s.browserLocation.components, + Error: s.browserLocation.lastError, + CapabilitiesVersion: browserlocation.Current().Version, } } @@ -481,14 +472,6 @@ func (s *ApiService) ResetBrowserLocationHTTP(w http.ResponseWriter, r *http.Req http.Error(w, err.Error(), http.StatusBadRequest) return } - validate := s.browserLocationValidate - if validate == nil { - validate = s.validateBrowserLocationSupport - } - if err := validate(r.Context(), validated); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } if err := s.resetBrowserLocation(reset.PreviousEpoch, validated); err != nil { http.Error(w, err.Error(), http.StatusConflict) return diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index 5054f8045..b71783598 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "log/slog" "net/http" "net/http/httptest" @@ -11,7 +12,6 @@ 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" @@ -23,6 +23,12 @@ func TestValidateBrowserLocationBundle(t *testing.T) { t.Skip("zoneinfo unavailable") } + for _, locale := range []string{"en-US", "de-DE", "pt-BR", "es-MX", "en-SG", "zh-TW"} { + raw := fmt.Sprintf(`{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":%q,"languages":[%q]}`, locale, locale) + bundle, err := validateBrowserLocationBundle(raw) + require.NoError(t, err, locale) + assert.Equal(t, locale, bundle.Locale) + } bundle, err := validateBrowserLocationBundle(`{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-US","languages":["en-US","en"]}`) require.NoError(t, err) assert.Equal(t, uint64(2), bundle.Generation) @@ -30,27 +36,17 @@ func TestValidateBrowserLocationBundle(t *testing.T) { for _, raw := range []string{ `{"epoch":"lease-1","generation":2,"timezone":"../../etc/passwd","locale":"en-US","languages":["en-US"]}`, + `{"epoch":"lease-1","generation":2,"timezone":"Mars/Olympus_Mons","locale":"en-US","languages":["en-US"]}`, `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-us","languages":["en-us"]}`, + `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-QQ","languages":["en-QQ"]}`, `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-US","languages":["de-DE"]}`, + `{"epoch":"lease-1","generation":2,"timezone":"America/New_York","locale":"en-US","languages":["en-US","EN"]}`, } { _, err := validateBrowserLocationBundle(raw) - assert.Error(t, err) + assert.Error(t, err, raw) } } -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 @@ -96,6 +92,30 @@ func TestBrowserLocationEpochOrdering(t *testing.T) { assert.ErrorIs(t, service.acceptBrowserLocation(conflict), errConflictBrowserLocation) } +func TestBrowserGenerationIncreasesAcrossEpochs(t *testing.T) { + service := newBrowserLocationStateService(t) + require.NoError(t, service.resetBrowserLocation("", testLocationBundle("lease-a", 1))) + assert.Equal(t, uint64(1), service.browserLocation.browserGeneration) + require.NoError(t, service.acceptBrowserLocation(testLocationBundle("lease-a", 2))) + assert.Equal(t, uint64(2), service.browserLocation.browserGeneration) + + // Retries keep the generation; a new lease epoch restarts bundle + // generations but not Chromium's. + require.NoError(t, service.acceptBrowserLocation(testLocationBundle("lease-a", 2))) + assert.Equal(t, uint64(2), service.browserLocation.browserGeneration) + require.NoError(t, service.resetBrowserLocation("lease-a", testLocationBundle("lease-b", 1))) + assert.Equal(t, uint64(3), service.browserLocation.browserGeneration) + + restored := &ApiService{lifecycleCtx: service.lifecycleCtx} + require.NoError(t, restored.initializeBrowserLocation()) + assert.Equal(t, uint64(3), restored.browserLocation.browserGeneration) + generation, current := restored.browserLocationGeneration(testLocationBundle("lease-b", 1)) + assert.True(t, current) + assert.Equal(t, uint64(3), generation) + _, current = restored.browserLocationGeneration(testLocationBundle("lease-a", 2)) + assert.False(t, current) +} + func TestBrowserLocationStateSurvivesAPIRestart(t *testing.T) { service := newBrowserLocationStateService(t) bundle := testLocationBundle("lease-a", 1) @@ -136,6 +156,11 @@ func TestBrowserLocationReconcilesAfterChromiumRestart(t *testing.T) { case <-time.After(3 * time.Second): t.Fatal("location was not reconciled after Chromium became ready") } + service.browserLocationMu.Lock() + applied := bundle + service.browserLocation.applied = &applied + service.browserLocation.components = browserLocationComponents{TimeZone: true, Browser: true, Renderers: true, NetworkContexts: true} + service.browserLocationMu.Unlock() _, err = file.WriteString("DevTools listening on ws://127.0.0.1:9222/devtools/browser/b\n") require.NoError(t, err) select { @@ -144,11 +169,13 @@ func TestBrowserLocationReconcilesAfterChromiumRestart(t *testing.T) { case <-time.After(3 * time.Second): t.Fatal("location was not reconciled after Chromium restart") } + status := service.browserLocationSnapshot() + assert.Nil(t, status.Applied, "a restarted Chromium must acknowledge the bundle again") + assert.Equal(t, browserLocationComponents{}, status.Components) } func TestResetBrowserLocationHTTPRequiresInstanceIdentity(t *testing.T) { service := newBrowserLocationStateService(t) - service.browserLocationValidate = func(context.Context, browserLocationBundle) error { return nil } t.Setenv("KERNEL_INSTANCE_JWT", "instance-token") body := `{"previous_epoch":"","bundle":{"epoch":"lease-a","generation":1,"timezone":"UTC","locale":"en-US","languages":["en-US","en"]}}` @@ -167,7 +194,6 @@ func TestResetBrowserLocationHTTPRequiresInstanceIdentity(t *testing.T) { 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)) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 84af29297..fc18a67ea 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -353,13 +353,6 @@ func (s *ApiService) applyBrowserLocationConfig(ctx context.Context, st *chromiu if st.browserLocation == nil { return nil } - validate := s.browserLocationValidate - if validate == nil { - validate = s.validateBrowserLocationSupport - } - if err := validate(ctx, *st.browserLocation); err != nil { - return oapi.ChromiumConfigure400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}} - } err := s.acceptBrowserLocation(*st.browserLocation) if err == nil { return nil diff --git a/server/lib/browserlocation/capabilities.go b/server/lib/browserlocation/capabilities.go new file mode 100644 index 000000000..5bf62d618 --- /dev/null +++ b/server/lib/browserlocation/capabilities.go @@ -0,0 +1,78 @@ +// Package browserlocation describes the explicit locale and timezone values +// that the browser and image shipped together can apply. +package browserlocation + +import ( + "crypto/sha256" + _ "embed" + "encoding/hex" + "encoding/json" + "fmt" + "slices" + "strings" +) + +//go:embed capabilities.json +var capabilitiesJSON []byte + +// Capabilities is generated by running the built browser inside the image +// (TestBrowserLocationCapabilities in server/e2e). A locale is listed when the +// browser's Intl.DateTimeFormat and Intl.NumberFormat resolve exactly that tag +// and Intl.Collator resolves it or a parent obtained by removing subtags, +// because collation data is mostly per language. A timezone is listed when the +// image ships its zoneinfo file and the browser's Intl accepts the identifier. +type Capabilities struct { + // Version identifies the browser, the image tzdata and the lists. + Version string `json:"version"` + Browser string `json:"browser"` + TZData string `json:"tzdata"` + Locales []string `json:"locales"` + TimeZones []string `json:"timezones"` +} + +var current = mustParse(capabilitiesJSON) + +// Current returns the capabilities of the browser and image this binary ships in. +func Current() Capabilities { + return current +} + +// New builds sorted capabilities and derives their version. +func New(browser, tzdata string, locales, timezones []string) Capabilities { + c := Capabilities{ + Browser: browser, + TZData: tzdata, + Locales: slices.Compact(slices.Sorted(slices.Values(locales))), + TimeZones: slices.Compact(slices.Sorted(slices.Values(timezones))), + } + c.Version = c.version() + return c +} + +// SupportsLocale reports whether tag is an accepted canonical locale. +func (c Capabilities) SupportsLocale(tag string) bool { + _, found := slices.BinarySearch(c.Locales, tag) + return found +} + +// SupportsTimeZone reports whether id is an accepted timezone identifier. +func (c Capabilities) SupportsTimeZone(id string) bool { + _, found := slices.BinarySearch(c.TimeZones, id) + return found +} + +func (c Capabilities) version() string { + sum := sha256.Sum256([]byte(strings.Join(c.Locales, ",") + "\n" + strings.Join(c.TimeZones, ","))) + return fmt.Sprintf("%s+tzdata-%s+%s", c.Browser, c.TZData, hex.EncodeToString(sum[:8])) +} + +func mustParse(data []byte) Capabilities { + var c Capabilities + if err := json.Unmarshal(data, &c); err != nil { + panic(fmt.Sprintf("decode browser location capabilities: %v", err)) + } + if !slices.IsSorted(c.Locales) || !slices.IsSorted(c.TimeZones) || c.Version != c.version() { + panic("browser location capabilities are not normalized; regenerate them") + } + return c +} diff --git a/server/lib/browserlocation/capabilities.json b/server/lib/browserlocation/capabilities.json new file mode 100644 index 000000000..c14e42522 --- /dev/null +++ b/server/lib/browserlocation/capabilities.json @@ -0,0 +1,1019 @@ +{ + "version": "Chromium/154.0.8037.97+tzdata-2026c+a1a4f3e6807fb867", + "browser": "Chromium/154.0.8037.97", + "tzdata": "2026c", + "locales": [ + "af", + "af-NA", + "af-ZA", + "am", + "am-ET", + "ar", + "ar-001", + "ar-AE", + "ar-BH", + "ar-DJ", + "ar-DZ", + "ar-EG", + "ar-EH", + "ar-ER", + "ar-IL", + "ar-IQ", + "ar-JO", + "ar-KM", + "ar-KW", + "ar-LB", + "ar-LY", + "ar-MA", + "ar-MR", + "ar-OM", + "ar-PS", + "ar-QA", + "ar-SA", + "ar-SD", + "ar-SO", + "ar-SS", + "ar-SY", + "ar-TD", + "ar-TN", + "ar-YE", + "bg", + "bg-BG", + "bn", + "bn-BD", + "bn-IN", + "ca", + "ca-AD", + "ca-ES", + "ca-FR", + "ca-IT", + "cs", + "cs-CZ", + "da", + "da-DK", + "da-GL", + "de", + "de-AT", + "de-BE", + "de-CH", + "de-DE", + "de-IT", + "de-LI", + "de-LU", + "el", + "el-CY", + "el-GR", + "en", + "en-001", + "en-150", + "en-AE", + "en-AG", + "en-AI", + "en-AS", + "en-AT", + "en-AU", + "en-BB", + "en-BE", + "en-BI", + "en-BM", + "en-BS", + "en-BW", + "en-BZ", + "en-CA", + "en-CC", + "en-CH", + "en-CK", + "en-CM", + "en-CX", + "en-CY", + "en-CZ", + "en-DE", + "en-DG", + "en-DK", + "en-DM", + "en-EE", + "en-ER", + "en-ES", + "en-FI", + "en-FJ", + "en-FK", + "en-FM", + "en-FR", + "en-GB", + "en-GD", + "en-GE", + "en-GG", + "en-GH", + "en-GI", + "en-GM", + "en-GS", + "en-GU", + "en-GY", + "en-HK", + "en-HU", + "en-ID", + "en-IE", + "en-IL", + "en-IM", + "en-IN", + "en-IO", + "en-IT", + "en-JE", + "en-JM", + "en-JP", + "en-KE", + "en-KI", + "en-KN", + "en-KY", + "en-LC", + "en-LR", + "en-LS", + "en-LT", + "en-LV", + "en-MG", + "en-MH", + "en-MO", + "en-MP", + "en-MS", + "en-MT", + "en-MU", + "en-MV", + "en-MW", + "en-MY", + "en-NA", + "en-NF", + "en-NG", + "en-NL", + "en-NO", + "en-NR", + "en-NU", + "en-NZ", + "en-PG", + "en-PH", + "en-PK", + "en-PL", + "en-PN", + "en-PR", + "en-PT", + "en-PW", + "en-RO", + "en-RW", + "en-SB", + "en-SC", + "en-SD", + "en-SE", + "en-SG", + "en-SH", + "en-SI", + "en-SK", + "en-SL", + "en-SS", + "en-SX", + "en-SZ", + "en-TC", + "en-TK", + "en-TO", + "en-TT", + "en-TV", + "en-TZ", + "en-UA", + "en-UG", + "en-UM", + "en-US", + "en-VC", + "en-VG", + "en-VI", + "en-VU", + "en-WS", + "en-ZA", + "en-ZM", + "en-ZW", + "es", + "es-419", + "es-AR", + "es-BO", + "es-BR", + "es-BZ", + "es-CL", + "es-CO", + "es-CR", + "es-CU", + "es-DO", + "es-EA", + "es-EC", + "es-ES", + "es-GQ", + "es-GT", + "es-HN", + "es-IC", + "es-MX", + "es-NI", + "es-PA", + "es-PE", + "es-PH", + "es-PR", + "es-PY", + "es-SV", + "es-US", + "es-UY", + "es-VE", + "et", + "et-EE", + "fa", + "fa-AF", + "fa-IR", + "fi", + "fi-FI", + "fil", + "fil-PH", + "fr", + "fr-BE", + "fr-BF", + "fr-BI", + "fr-BJ", + "fr-BL", + "fr-CA", + "fr-CD", + "fr-CF", + "fr-CG", + "fr-CH", + "fr-CI", + "fr-CM", + "fr-DJ", + "fr-DZ", + "fr-FR", + "fr-GA", + "fr-GF", + "fr-GN", + "fr-GP", + "fr-GQ", + "fr-HT", + "fr-KM", + "fr-LU", + "fr-MA", + "fr-MC", + "fr-MF", + "fr-MG", + "fr-ML", + "fr-MQ", + "fr-MR", + "fr-MU", + "fr-NC", + "fr-NE", + "fr-PF", + "fr-PM", + "fr-RE", + "fr-RW", + "fr-SC", + "fr-SN", + "fr-SY", + "fr-TD", + "fr-TG", + "fr-TN", + "fr-VU", + "fr-WF", + "fr-YT", + "gu", + "gu-IN", + "he", + "he-IL", + "hi", + "hi-IN", + "hi-Latn", + "hi-Latn-IN", + "hr", + "hr-BA", + "hr-HR", + "hu", + "hu-HU", + "id", + "id-ID", + "it", + "it-CH", + "it-IT", + "it-SM", + "it-VA", + "ja", + "ja-JP", + "kn", + "kn-IN", + "ko", + "ko-CN", + "ko-KP", + "ko-KR", + "lt", + "lt-LT", + "lv", + "lv-LV", + "ml", + "ml-IN", + "mr", + "mr-IN", + "ms", + "ms-BN", + "ms-ID", + "ms-MY", + "ms-SG", + "nb", + "nl", + "nl-AW", + "nl-BE", + "nl-BQ", + "nl-CW", + "nl-NL", + "nl-SR", + "nl-SX", + "no", + "no-NO", + "pl", + "pl-PL", + "pt", + "pt-AO", + "pt-BR", + "pt-CH", + "pt-CV", + "pt-GQ", + "pt-GW", + "pt-LU", + "pt-MO", + "pt-MZ", + "pt-PT", + "pt-ST", + "pt-TL", + "ro", + "ro-MD", + "ro-RO", + "ru", + "ru-BY", + "ru-KG", + "ru-KZ", + "ru-MD", + "ru-RU", + "ru-UA", + "sk", + "sk-SK", + "sl", + "sl-SI", + "sr", + "sr-BA", + "sr-Cyrl", + "sr-Cyrl-BA", + "sr-Cyrl-ME", + "sr-Cyrl-RS", + "sr-Cyrl-XK", + "sr-Latn", + "sr-Latn-BA", + "sr-Latn-ME", + "sr-Latn-RS", + "sr-Latn-XK", + "sr-ME", + "sr-RS", + "sr-XK", + "sv", + "sv-AX", + "sv-FI", + "sv-SE", + "sw", + "sw-CD", + "sw-KE", + "sw-TZ", + "sw-UG", + "ta", + "ta-IN", + "ta-LK", + "ta-MY", + "ta-SG", + "te", + "te-IN", + "th", + "th-TH", + "tr", + "tr-CY", + "tr-TR", + "uk", + "uk-UA", + "ur", + "ur-IN", + "ur-PK", + "vi", + "vi-VN", + "zh", + "zh-CN", + "zh-HK", + "zh-Hans", + "zh-Hans-CN", + "zh-Hans-HK", + "zh-Hans-MO", + "zh-Hans-MY", + "zh-Hans-SG", + "zh-Hant", + "zh-Hant-HK", + "zh-Hant-MO", + "zh-Hant-MY", + "zh-Hant-TW", + "zh-MO", + "zh-MY", + "zh-SG", + "zh-TW" + ], + "timezones": [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Coyhaique", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" + ] +} diff --git a/server/lib/browserlocation/capabilities_test.go b/server/lib/browserlocation/capabilities_test.go new file mode 100644 index 000000000..0551e42eb --- /dev/null +++ b/server/lib/browserlocation/capabilities_test.go @@ -0,0 +1,38 @@ +package browserlocation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCurrentCapabilitiesAreNormalized(t *testing.T) { + c := Current() + assert.Equal(t, New(c.Browser, c.TZData, c.Locales, c.TimeZones), c) + assert.NotEmpty(t, c.Browser) + assert.NotEmpty(t, c.TZData) +} + +func TestCurrentCapabilitiesCoverRepresentativeValues(t *testing.T) { + c := Current() + for _, locale := range []string{"en-US", "en-SG", "de-DE", "pt-BR", "es-MX", "zh-TW", "ja-JP"} { + assert.True(t, c.SupportsLocale(locale), locale) + } + for _, locale := range []string{"en-QQ", "de-de", "en_US", "", "zh-Hant-XX"} { + assert.False(t, c.SupportsLocale(locale), locale) + } + for _, timezone := range []string{"America/Los_Angeles", "America/New_York", "Europe/Berlin", "Asia/Singapore", "Etc/UTC"} { + assert.True(t, c.SupportsTimeZone(timezone), timezone) + } + for _, timezone := range []string{"Mars/Olympus_Mons", "../etc/passwd", "posix/Europe/Berlin", ""} { + assert.False(t, c.SupportsTimeZone(timezone), timezone) + } +} + +func TestNewSortsAndDeduplicates(t *testing.T) { + c := New("Chrome/1.0", "2026a", []string{"fr-FR", "de-DE", "fr-FR"}, []string{"Etc/UTC", "Europe/Berlin", "Etc/UTC"}) + assert.Equal(t, []string{"de-DE", "fr-FR"}, c.Locales) + assert.Equal(t, []string{"Etc/UTC", "Europe/Berlin"}, c.TimeZones) + assert.Contains(t, c.Version, "Chrome/1.0+tzdata-2026a+") + assert.NotEqual(t, c.Version, New("Chrome/1.0", "2026a", []string{"de-DE"}, c.TimeZones).Version) +} diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index 526c19347..99c657b21 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -817,41 +817,24 @@ func (c *Client) SetDeviceMetricsOverride(ctx context.Context, width, height int return nil } -// BrowserLocation is Chromium's current browser-owned geography state. +// BrowserLocation is Chromium's browser-owned location state and the +// acknowledgements every component has returned for Generation. type BrowserLocation struct { + Generation uint64 `json:"generation"` Locale string `json:"locale"` AcceptLanguages string `json:"acceptLanguages"` TimeZone string `json:"timezone"` - DateTimeLocale string `json:"dateTimeLocale"` - NumberLocale string `json:"numberLocale"` - CollatorLocale string `json:"collatorLocale"` RenderersConverged bool `json:"renderersConverged"` + TimeZoneConverged bool `json:"timezoneConverged"` NetworkContextsConverged bool `json:"networkContextsConverged"` } -type BrowserLocationResolution struct { - DateTimeLocale string `json:"dateTimeLocale"` - NumberLocale string `json:"numberLocale"` - CollatorLocale string `json:"collatorLocale"` -} - -// ValidateBrowserLocation verifies locale behavior against Chromium's shipped ICU data. -func (c *Client) ValidateBrowserLocation(ctx context.Context, locale string) (BrowserLocationResolution, error) { - raw, err := c.Send(ctx, "Browser.validateKernelBrowserLocation", map[string]string{"locale": locale}, "") - if err != nil { - return BrowserLocationResolution{}, fmt.Errorf("Browser.validateKernelBrowserLocation: %w", err) - } - var resolution BrowserLocationResolution - if err := json.Unmarshal(raw, &resolution); err != nil { - return BrowserLocationResolution{}, fmt.Errorf("decode browser location validation: %w", err) - } - return resolution, nil -} - -// SetBrowserLocation updates Chromium's session-only locale and language defaults. -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, "timezone": timezone, +// SetBrowserLocation updates Chromium's session-only locale and language +// defaults. Generations must increase; repeating the current generation with +// the same values retries renderers whose host timezone has not converged. +func (c *Client) SetBrowserLocation(ctx context.Context, generation uint64, locale, acceptLanguages, timezone string) error { + _, err := c.Send(ctx, "Browser.setKernelBrowserLocation", map[string]any{ + "generation": generation, "locale": locale, "acceptLanguages": acceptLanguages, "timezone": timezone, }, "") if err != nil { return fmt.Errorf("Browser.setKernelBrowserLocation: %w", err) @@ -859,7 +842,7 @@ func (c *Client) SetBrowserLocation(ctx context.Context, locale, acceptLanguages return nil } -// GetBrowserLocation reads Chromium's observed locale, languages and host timezone. +// GetBrowserLocation reads Chromium's location state and acknowledgements. func (c *Client) GetBrowserLocation(ctx context.Context) (BrowserLocation, error) { raw, err := c.Send(ctx, "Browser.getKernelBrowserLocation", nil, "") if err != nil { diff --git a/server/lib/cdpclient/cdpclient_test.go b/server/lib/cdpclient/cdpclient_test.go index fa04ca416..ff556c922 100644 --- a/server/lib/cdpclient/cdpclient_test.go +++ b/server/lib/cdpclient/cdpclient_test.go @@ -18,43 +18,41 @@ import ( // fakeCDP is a minimal CDP server that responds to the commands used by // SetDeviceMetricsOverride and GetBrowserVersion. type fakeCDP struct { - getTargetsCalls int - attachCalled bool - setMetricsCalled bool - setMetricsWidth int - setMetricsHeight int - detachCalled bool - pageTargetID string - sessionID string - failGetTargets bool - failSetMetrics bool - returnNoPageTargets bool - noPageTargetsFor int - getVersionCalled bool - failGetVersion bool - productResponse string - browserLocale string - browserLanguages string - browserTimezone string - browserDateTimeLocale string - browserNumberLocale string - browserCollatorLocale string - loadUnpackedCalled bool - loadUnpackedPath string - loadUnpackedID string - failLoadUnpacked bool - getExtensionsCalled bool - extensions []ExtensionInfo - failGetExtensions bool - navigateCalled bool - navigateCalls int - navigateURL string - pageStates []string - pageStateIndex int - createdTargetID string - createParams map[string]any - closedTargetID string - closeTargetMissing bool + getTargetsCalls int + attachCalled bool + setMetricsCalled bool + setMetricsWidth int + setMetricsHeight int + detachCalled bool + pageTargetID string + sessionID string + failGetTargets bool + failSetMetrics bool + returnNoPageTargets bool + noPageTargetsFor int + getVersionCalled bool + failGetVersion bool + productResponse string + browserGeneration uint64 + browserLocale string + browserLanguages string + browserTimezone string + loadUnpackedCalled bool + loadUnpackedPath string + loadUnpackedID string + failLoadUnpacked bool + getExtensionsCalled bool + extensions []ExtensionInfo + failGetExtensions bool + navigateCalled bool + navigateCalls int + navigateURL string + pageStates []string + pageStateIndex int + createdTargetID string + createParams map[string]any + closedTargetID string + closeTargetMissing bool } func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { @@ -141,17 +139,21 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) { "jsVersion": "1.2.3", } } - case "Browser.validateKernelBrowserLocation": - result = map[string]any{"dateTimeLocale": f.browserDateTimeLocale, "numberLocale": f.browserNumberLocale, "collatorLocale": f.browserCollatorLocale} case "Browser.setKernelBrowserLocation": - var params map[string]string + var params struct { + Generation uint64 `json:"generation"` + Locale string `json:"locale"` + AcceptLanguages string `json:"acceptLanguages"` + TimeZone string `json:"timezone"` + } _ = json.Unmarshal(req.Params, ¶ms) - f.browserLocale = params["locale"] - f.browserLanguages = params["acceptLanguages"] - f.browserTimezone = params["timezone"] + f.browserGeneration = params.Generation + 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} + result = map[string]any{"generation": f.browserGeneration, "locale": f.browserLocale, "acceptLanguages": f.browserLanguages, "timezone": f.browserTimezone, "renderersConverged": true, "timezoneConverged": true, "networkContextsConverged": true} case "Extensions.loadUnpacked": f.loadUnpackedCalled = true var params map[string]string @@ -844,18 +846,14 @@ func TestTargetURLs(t *testing.T) { } func TestBrowserLocation(t *testing.T) { - f := &fakeCDP{browserTimezone: "Europe/Berlin", browserDateTimeLocale: "de", browserNumberLocale: "de", browserCollatorLocale: "de"} + f := &fakeCDP{} url := startFakeCDP(t, f) client, err := Dial(context.Background(), url) require.NoError(t, err) defer client.Close() - resolution, err := client.ValidateBrowserLocation(context.Background(), "de-DE") - 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", "Europe/Berlin")) + require.NoError(t, client.SetBrowserLocation(context.Background(), 7, "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) + assert.Equal(t, BrowserLocation{Generation: 7, Locale: "de-DE", AcceptLanguages: "de-DE,de", TimeZone: "Europe/Berlin", RenderersConverged: true, TimeZoneConverged: true, NetworkContextsConverged: true}, location) } From 98b8e63dfe0ecf33d31ad37c9b42ed37f202600f Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:37:12 +0000 Subject: [PATCH 08/11] Cover runtime timezone changes and capability generation end to end TestBrowserTimezoneFollowsLocaltime starts Chromium with the production TZ and KERNEL_BROWSER_TIMEZONE environment, checks Chromium runs without TZ, and replaces /etc/localtime A to B at runtime without a restart. TestBrowserLocationCapabilities regenerates or checks the capability manifest. TestBrowserLocationRuntimeUpdate drives reset, configure, retries, stale generations, Chromium restart and a new lease on a kernel-browser image. --- .../e2e_browser_location_capabilities_test.go | 211 ++++++++++++ .../e2e/e2e_browser_location_runtime_test.go | 303 ++++++++++++++++++ 2 files changed, 514 insertions(+) create mode 100644 server/e2e/e2e_browser_location_capabilities_test.go create mode 100644 server/e2e/e2e_browser_location_runtime_test.go diff --git a/server/e2e/e2e_browser_location_capabilities_test.go b/server/e2e/e2e_browser_location_capabilities_test.go new file mode 100644 index 000000000..5ab084a2b --- /dev/null +++ b/server/e2e/e2e_browser_location_capabilities_test.go @@ -0,0 +1,211 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/browserlocation" + "github.com/stretchr/testify/require" +) + +// requireKernelBrowser skips tests that need Kernel's browser-location CDP +// commands, which stock Chrome for Testing images do not have. +func requireKernelBrowser(t *testing.T) { + t.Helper() + if os.Getenv("E2E_KERNEL_BROWSER") != "1" { + t.Skip("set E2E_KERNEL_BROWSER=1 when the image ships a kernel-browser build") + } +} + +// capabilityLocalesScript lists the canonical locales the browser resolves +// exactly: Intl.DateTimeFormat and Intl.NumberFormat must resolve the tag +// itself, and Intl.Collator the tag or a parent obtained by removing subtags. +// Candidates are the language, script and region codes Intl.DisplayNames +// knows; resolution then decides. +const capabilityLocalesScript = `(() => { + const exact = tag => { + try { + return Intl.getCanonicalLocales(tag)[0] === tag && + new Intl.DateTimeFormat(tag).resolvedOptions().locale === tag && + new Intl.NumberFormat(tag).resolvedOptions().locale === tag; + } catch { + return false; + } + }; + const collates = tag => { + const resolved = new Intl.Collator(tag).resolvedOptions().locale; + return resolved === tag || tag.startsWith(resolved + '-'); + }; + const names = type => new Intl.DisplayNames('en', {type, fallback: 'none'}); + const known = (displayNames, code) => { + try { + return displayNames.of(code) !== undefined; + } catch { + return false; + } + }; + const languageNames = names('language'); + const regionNames = names('region'); + const scriptNames = names('script'); + const letters = 'abcdefghijklmnopqrstuvwxyz'; + const codes = []; + for (const a of letters) for (const b of letters) { + codes.push(a + b); + for (const c of letters) codes.push(a + b + c); + } + const languages = codes.filter(code => known(languageNames, code)).filter(exact); + const regions = codes.filter(code => code.length === 2).map(code => code.toUpperCase()).filter(code => known(regionNames, code)); + regions.push('001', '150', '419'); + const scripts = []; + for (const a of letters.toUpperCase()) for (const b of letters) for (const c of letters) for (const d of letters) { + if (known(scriptNames, a + b + c + d)) scripts.push(a + b + c + d); + } + const prefixes = [...languages]; + for (const language of languages) for (const script of scripts) { + if (exact(language + '-' + script)) prefixes.push(language + '-' + script); + } + const accepted = []; + for (const prefix of prefixes) { + if (collates(prefix)) accepted.push(prefix); + for (const region of regions) { + const tag = prefix + '-' + region; + if (exact(tag) && collates(tag)) accepted.push(tag); + } + } + return JSON.stringify(accepted); +})()` + +const capabilityTimeZonesScript = `(ids => JSON.stringify(ids.filter(id => { + try { + new Intl.DateTimeFormat('en', {timeZone: id}); + return true; + } catch { + return false; + } +})))` + +// TestBrowserLocationCapabilities regenerates the capability manifest from the +// running browser and image. Set UPDATE_BROWSER_LOCATION_CAPABILITIES=1 to +// write it; otherwise the test fails when the committed manifest is stale for +// the same Chromium version, and skips images with a different version. +func TestBrowserLocationCapabilities(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{}), "failed to start container") + defer c.Stop(ctx) + require.NoError(t, c.WaitReady(ctx), "api not ready") + require.NoError(t, c.WaitDevTools(ctx), "devtools not ready") + + page, err := openLocationPage(ctx, c.CDPURL()) + require.NoError(t, err) + defer page.Close(ctx) + + versionRaw, err := page.client.Call(ctx, "Browser.getVersion", nil, "") + require.NoError(t, err) + product, err := decodeJSONStringField(versionRaw, "product") + require.NoError(t, err) + _, version, ok := strings.Cut(product, "/") + require.True(t, ok, "unexpected product %q", product) + // Locale support depends on the Chromium version's ICU data, not on the + // product name, which differs between builds of the same version. + browser := "Chromium/" + version + update := os.Getenv("UPDATE_BROWSER_LOCATION_CAPABILITIES") == "1" + if !update && browser != browserlocation.Current().Browser { + t.Skipf("capabilities describe %s, image runs %s", browserlocation.Current().Browser, browser) + } + + var locales []string + require.NoError(t, page.evalJSON(ctx, capabilityLocalesScript, &locales)) + + tzdata, err := execCombinedOutput(ctx, c, "sh", []string{"-c", `sed -n 's/^# version //p' /usr/share/zoneinfo/tzdata.zi`}) + require.NoError(t, err) + zoneFiles, err := execCombinedOutput(ctx, c, "sh", []string{"-c", `cd /usr/share/zoneinfo && find . -path ./posix -prune -o -path ./right -prune -o \( -type f -o -type l \) -print | sed 's|^\./||' | grep -v '\.'`}) + require.NoError(t, err) + candidates, err := json.Marshal(strings.Fields(zoneFiles)) + require.NoError(t, err) + var timezones []string + require.NoError(t, page.evalJSON(ctx, fmt.Sprintf("%s(%s)", capabilityTimeZonesScript, candidates), &timezones)) + + generated := browserlocation.New(browser, strings.TrimSpace(tzdata), locales, timezones) + if update { + data, err := json.MarshalIndent(generated, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile("../lib/browserlocation/capabilities.json", append(data, '\n'), 0o644)) + t.Logf("wrote %d locales and %d timezones (%s)", len(generated.Locales), len(generated.TimeZones), generated.Version) + return + } + require.Equal(t, generated, browserlocation.Current(), "capability manifest is stale; regenerate it with UPDATE_BROWSER_LOCATION_CAPABILITIES=1") +} + +// locationPage is a CDP session attached to a blank page. +type locationPage struct { + client *cdpClient + targetID string + sessionID string +} + +func openLocationPage(ctx context.Context, wsURL string) (*locationPage, error) { + client, err := newCDPClient(ctx, wsURL) + if err != nil { + return nil, err + } + targetRaw, err := client.Call(ctx, "Target.createTarget", map[string]any{"url": "about:blank"}, "") + if err != nil { + client.Close() + return nil, fmt.Errorf("Target.createTarget: %w", err) + } + targetID, err := decodeJSONStringField(targetRaw, "targetId") + if err != nil { + client.Close() + return nil, err + } + attachRaw, err := client.Call(ctx, "Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "") + if err != nil { + client.Close() + return nil, fmt.Errorf("Target.attachToTarget: %w", err) + } + sessionID, err := decodeJSONStringField(attachRaw, "sessionId") + if err != nil { + client.Close() + return nil, err + } + return &locationPage{client: client, targetID: targetID, sessionID: sessionID}, nil +} + +// evalJSON evaluates an expression that yields a JSON string, awaiting +// promises, and decodes it into out. +func (p *locationPage) evalJSON(ctx context.Context, expression string, out any) error { + raw, err := p.client.Call(ctx, "Runtime.evaluate", map[string]any{ + "expression": expression, + "returnByValue": true, + "awaitPromise": true, + }, p.sessionID) + if err != nil { + return fmt.Errorf("Runtime.evaluate: %w", err) + } + var envelope struct { + Result struct { + Value string `json:"value"` + } `json:"result"` + ExceptionDetails json.RawMessage `json:"exceptionDetails"` + } + if err := json.Unmarshal(raw, &envelope); err != nil { + return fmt.Errorf("decode Runtime.evaluate result: %w", err) + } + if len(envelope.ExceptionDetails) > 0 { + return fmt.Errorf("evaluation raised an exception: %s", envelope.ExceptionDetails) + } + return json.Unmarshal([]byte(envelope.Result.Value), out) +} + +func (p *locationPage) Close(ctx context.Context) { + _, _ = p.client.Call(ctx, "Target.closeTarget", map[string]any{"targetId": p.targetID}, "") + p.client.Close() +} diff --git a/server/e2e/e2e_browser_location_runtime_test.go b/server/e2e/e2e_browser_location_runtime_test.go new file mode 100644 index 000000000..ad823df30 --- /dev/null +++ b/server/e2e/e2e_browser_location_runtime_test.go @@ -0,0 +1,303 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + "testing" + "time" + _ "time/tzdata" + + "github.com/stretchr/testify/require" +) + +const locationInstanceToken = "e2e-instance-token" + +// locationProbeScript starts a dedicated worker on first use and reports the +// page's and the worker's timezone, locale and language. The worker keeps +// running across updates so it is an already-running recipient. +const locationProbeScript = `(async () => { + const probe = () => ({ + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + locale: Intl.DateTimeFormat().resolvedOptions().locale, + language: navigator.language, + offset: new Date().getTimezoneOffset(), + }); + if (!window.__locationWorker) { + window.__locationWorker = new Worker(URL.createObjectURL(new Blob([ + 'onmessage=()=>postMessage({timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,locale:Intl.DateTimeFormat().resolvedOptions().locale,language:navigator.language,offset:new Date().getTimezoneOffset()})' + ], {type: 'text/javascript'}))); + } + const worker = await new Promise((resolve, reject) => { + __locationWorker.onmessage = event => resolve(event.data); + __locationWorker.onerror = reject; + __locationWorker.postMessage(null); + }); + return JSON.stringify({page: probe(), worker}); +})()` + +type locationObservation struct { + TimeZone string `json:"timeZone"` + Locale string `json:"locale"` + Language string `json:"language"` + Offset int `json:"offset"` +} + +type locationProbeResult struct { + Page locationObservation `json:"page"` + Worker locationObservation `json:"worker"` +} + +type imageLocationBundle struct { + Epoch string `json:"epoch"` + Generation uint64 `json:"generation"` + TimeZone string `json:"timezone"` + Locale string `json:"locale"` + Languages []string `json:"languages"` +} + +type imageLocationStatus struct { + ActiveEpoch string `json:"active_epoch"` + Accepted *imageLocationBundle `json:"accepted"` + Applied *imageLocationBundle `json:"applied"` + Components map[string]bool `json:"components"` + Error string `json:"error"` +} + +// TestBrowserTimezoneFollowsLocaltime starts Chromium with the production +// environment and replaces /etc/localtime at runtime, the way the image applies +// a timezone: Chromium can miss the first replacement after it starts, so the +// image rewrites the same target until renderers observe it. The test does the +// same. It needs no Kernel CDP commands, so it runs on every image. +func TestBrowserTimezoneFollowsLocaltime(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{Env: map[string]string{ + "TZ": "America/Los_Angeles", + "KERNEL_BROWSER_TIMEZONE": "America/Los_Angeles", + "CHROMIUM_FLAGS": "--remote-allow-origins=*", + }}), "failed to start container") + defer c.Stop(ctx) + require.NoError(t, c.WaitReady(ctx), "api not ready") + require.NoError(t, c.WaitDevTools(ctx), "devtools not ready") + + pid, started := chromiumProcess(ctx, t, c) + requireChromiumWithoutTZ(ctx, t, c, pid) + + page, err := openLocationPage(ctx, c.CDPURL()) + require.NoError(t, err) + defer page.Close(ctx) + waitTimeZone(ctx, t, page, "America/Los_Angeles", nil) + + // Includes a same-value change, which must leave the browser converged. + for _, timezone := range []string{"Asia/Singapore", "Europe/Berlin", "Europe/Berlin", "America/New_York"} { + replace := func() { + _, err := execCombinedOutput(ctx, c, "sh", []string{"-c", fmt.Sprintf(`ln -s /usr/share/zoneinfo/%s /etc/.localtime-e2e && mv -fT /etc/.localtime-e2e /etc/localtime`, timezone)}) + require.NoError(t, err) + } + replace() + waitTimeZone(ctx, t, page, timezone, replace) + } + + restartedPID, restartedStart := chromiumProcess(ctx, t, c) + require.Equal(t, pid, restartedPID, "timezone changes must not restart Chromium") + require.Equal(t, started, restartedStart, "timezone changes must not restart Chromium") +} + +func requireChromiumWithoutTZ(ctx context.Context, t *testing.T, c *TestContainer, pid string) { + t.Helper() + environment, err := execCombinedOutput(ctx, c, "sh", []string{"-c", fmt.Sprintf(`tr '\0' '\n' < /proc/%s/environ | grep '^TZ=' || true`, pid)}) + require.NoError(t, err) + require.Empty(t, strings.TrimSpace(environment), "Chromium must start without TZ so it watches /etc/localtime") +} + +// waitTimeZone polls until the page and its already-running worker report +// timezone with the matching UTC offset, calling rewrite every 250ms if set. +func waitTimeZone(ctx context.Context, t *testing.T, page *locationPage, timezone string, rewrite func()) { + t.Helper() + zone, err := time.LoadLocation(timezone) + require.NoError(t, err) + _, offsetSeconds := time.Now().In(zone).Zone() + deadline := time.Now().Add(10 * time.Second) + started := time.Now() + lastRewrite := started + rewrites := 0 + var observed locationProbeResult + for time.Now().Before(deadline) { + require.NoError(t, page.evalJSON(ctx, locationProbeScript, &observed)) + if observed.Page.TimeZone == timezone && observed.Worker.TimeZone == timezone && + observed.Page.Offset == -offsetSeconds/60 && observed.Worker.Offset == -offsetSeconds/60 { + t.Logf("%s converged in %v after %d rewrites", timezone, time.Since(started).Round(time.Millisecond), rewrites) + return + } + if rewrite != nil && time.Since(lastRewrite) >= 250*time.Millisecond { + rewrite() + rewrites++ + lastRewrite = time.Now() + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("browser did not follow /etc/localtime to %s: %+v", timezone, observed) +} + +// TestBrowserLocationRuntimeUpdate starts Chromium with the production +// environment, which sets both TZ and KERNEL_BROWSER_TIMEZONE, and changes its +// timezone and locale at runtime without restarting it. +func TestBrowserLocationRuntimeUpdate(t *testing.T) { + requireKernelBrowser(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{Env: map[string]string{ + "TZ": "America/Los_Angeles", + "KERNEL_BROWSER_TIMEZONE": "America/Los_Angeles", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US.UTF-8", + "CHROMIUM_FLAGS": "--lang=en-US --accept-lang=en-US,en --remote-allow-origins=*", + "KERNEL_INSTANCE_JWT": locationInstanceToken, + }}), "failed to start container") + defer c.Stop(ctx) + require.NoError(t, c.WaitReady(ctx), "api not ready") + require.NoError(t, c.WaitDevTools(ctx), "devtools not ready") + + pid, started := chromiumProcess(ctx, t, c) + requireChromiumWithoutTZ(ctx, t, c, pid) + + page, err := openLocationPage(ctx, c.CDPURL()) + require.NoError(t, err) + defer page.Close(ctx) + requireLocation(ctx, t, c, page, "America/Los_Angeles", "en-US") + + // A to B through the lease-reset endpoint, then B to C through configure. + singapore := imageLocationBundle{Epoch: "lease-a", Generation: 1, TimeZone: "Asia/Singapore", Locale: "en-SG", Languages: []string{"en-SG", "en"}} + resetLocation(ctx, t, c, "", singapore) + waitLocationApplied(ctx, t, c, singapore) + requireLocation(ctx, t, c, page, "Asia/Singapore", "en-SG") + + berlin := imageLocationBundle{Epoch: "lease-a", Generation: 2, TimeZone: "Europe/Berlin", Locale: "de-DE", Languages: []string{"de-DE", "de"}} + configureLocation(ctx, t, c, berlin, http.StatusOK) + waitLocationApplied(ctx, t, c, berlin) + requireLocation(ctx, t, c, page, "Europe/Berlin", "de-DE") + + // Same-value retry and stale generation. + configureLocation(ctx, t, c, berlin, http.StatusOK) + waitLocationApplied(ctx, t, c, berlin) + configureLocation(ctx, t, c, singapore, http.StatusConflict) + + restartedPID, restartedStart := chromiumProcess(ctx, t, c) + require.Equal(t, pid, restartedPID, "location updates must not restart Chromium") + require.Equal(t, started, restartedStart, "location updates must not restart Chromium") + + // A restarted Chromium starts from the launcher's startup timezone and must + // converge back to the accepted bundle. + _, err = execCombinedOutput(ctx, c, "supervisorctl", []string{"-c", "/etc/supervisor/supervisord.conf", "restart", "chromium"}) + require.NoError(t, err) + require.NoError(t, c.WaitDevTools(ctx), "devtools not ready after restart") + waitLocationApplied(ctx, t, c, berlin) + restartedPage, err := openLocationPage(ctx, c.CDPURL()) + require.NoError(t, err) + defer restartedPage.Close(ctx) + requireLocation(ctx, t, c, restartedPage, "Europe/Berlin", "de-DE") + + // A new lease epoch restarts bundle generations; Chromium's keep increasing. + newYork := imageLocationBundle{Epoch: "lease-b", Generation: 1, TimeZone: "America/New_York", Locale: "en-US", Languages: []string{"en-US", "en"}} + resetLocation(ctx, t, c, "lease-a", newYork) + waitLocationApplied(ctx, t, c, newYork) + requireLocation(ctx, t, c, restartedPage, "America/New_York", "en-US") +} + +// chromiumProcess returns the browser process ID and its start time in clock +// ticks since boot. +func chromiumProcess(ctx context.Context, t *testing.T, c *TestContainer) (string, string) { + t.Helper() + out, err := execCombinedOutput(ctx, c, "sh", []string{"-c", `pid=$(pgrep -o -x chromium || pgrep -o -x chrome) && echo "$pid $(cut -d' ' -f22 /proc/$pid/stat)"`}) + require.NoError(t, err) + fields := strings.Fields(out) + require.Len(t, fields, 2, "unexpected process output %q", out) + return fields[0], fields[1] +} + +// requireLocation checks the page, its already-running worker and the OS +// without polling: the image reports a bundle applied only after every +// existing recipient has observed it. +func requireLocation(ctx context.Context, t *testing.T, c *TestContainer, page *locationPage, timezone, locale string) { + t.Helper() + zone, err := time.LoadLocation(timezone) + require.NoError(t, err) + _, offsetSeconds := time.Now().In(zone).Zone() + want := locationObservation{TimeZone: timezone, Locale: locale, Language: locale, Offset: -offsetSeconds / 60} + + var observed locationProbeResult + require.NoError(t, page.evalJSON(ctx, locationProbeScript, &observed)) + require.Equal(t, want, observed.Page, "page") + require.Equal(t, want, observed.Worker, "existing worker") + + // The container environment still carries the startup TZ, so read the + // system timezone without it. + date, err := execCombinedOutput(ctx, c, "env", []string{"-u", "TZ", "date", "+%z"}) + require.NoError(t, err) + require.Equal(t, time.Now().In(zone).Format("-0700"), strings.TrimSpace(date)) +} + +func resetLocation(ctx context.Context, t *testing.T, c *TestContainer, previousEpoch string, bundle imageLocationBundle) { + t.Helper() + body, err := json.Marshal(map[string]any{"previous_epoch": previousEpoch, "bundle": bundle}) + require.NoError(t, err) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.APIBaseURL()+"/internal/browser-location/reset", bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+locationInstanceToken) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + require.Equal(t, http.StatusAccepted, resp.StatusCode, string(respBody)) +} + +func configureLocation(ctx context.Context, t *testing.T, c *TestContainer, bundle imageLocationBundle, wantStatus int) { + t.Helper() + payload, err := json.Marshal(bundle) + require.NoError(t, err) + var body bytes.Buffer + writer := multipart.NewWriter(&body) + require.NoError(t, writer.WriteField("browser_location", string(payload))) + require.NoError(t, writer.Close()) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.APIBaseURL()+"/chromium/configure", &body) + require.NoError(t, err) + req.Header.Set("Content-Type", writer.FormDataContentType()) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + require.Equal(t, wantStatus, resp.StatusCode, string(respBody)) +} + +func waitLocationApplied(ctx context.Context, t *testing.T, c *TestContainer, bundle imageLocationBundle) { + t.Helper() + deadline := time.Now().Add(15 * time.Second) + var status imageLocationStatus + for time.Now().Before(deadline) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.APIBaseURL()+"/browser/location", nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + err = json.NewDecoder(resp.Body).Decode(&status) + resp.Body.Close() + require.NoError(t, err) + if status.Applied != nil && status.Applied.Epoch == bundle.Epoch && status.Applied.Generation == bundle.Generation { + for _, component := range []string{"timezone", "browser", "renderers", "network_contexts"} { + require.True(t, status.Components[component], "applied without %s convergence: %+v", component, status) + } + return + } + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("browser location %s/%d was not applied: %+v", bundle.Epoch, bundle.Generation, status) +} From 9fa53d11ab109f783f1a2cfa1e77169089cc62be Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:39:06 +0000 Subject: [PATCH 09/11] Drop the removed validate command from the internal CDP block list --- server/lib/devtoolsproxy/proxy.go | 5 ++--- server/lib/devtoolsproxy/proxy_test.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/lib/devtoolsproxy/proxy.go b/server/lib/devtoolsproxy/proxy.go index 923a3759d..34c816b9a 100644 --- a/server/lib/devtoolsproxy/proxy.go +++ b/server/lib/devtoolsproxy/proxy.go @@ -28,9 +28,8 @@ import ( ) var internalCDPMethods = map[string]struct{}{ - "Browser.validateKernelBrowserLocation": {}, - "Browser.setKernelBrowserLocation": {}, - "Browser.getKernelBrowserLocation": {}, + "Browser.setKernelBrowserLocation": {}, + "Browser.getKernelBrowserLocation": {}, } func filterInternalCDPMethod(message []byte) []byte { diff --git a/server/lib/devtoolsproxy/proxy_test.go b/server/lib/devtoolsproxy/proxy_test.go index d8e00182e..4421e4643 100644 --- a/server/lib/devtoolsproxy/proxy_test.go +++ b/server/lib/devtoolsproxy/proxy_test.go @@ -868,7 +868,7 @@ func TestWebSocketProxyHandler_EmitsUpstreamErrorOnDialFailure(t *testing.T) { func controlOn() bool { return true } func TestFilterInternalCDPMethod(t *testing.T) { - for _, method := range []string{"Browser.validateKernelBrowserLocation", "Browser.setKernelBrowserLocation", "Browser.getKernelBrowserLocation"} { + for _, method := range []string{"Browser.setKernelBrowserLocation", "Browser.getKernelBrowserLocation"} { input := []byte(`{"id":7,"method":"` + method + `","params":{"locale":"de-DE"}}`) var got map[string]any if err := json.Unmarshal(filterInternalCDPMethod(input), &got); err != nil { From b743ced10aed03959649fd58fc296aa6132c68ec Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:16:05 +0000 Subject: [PATCH 10/11] Keep the accepted timezone across Chromium restarts and confirm applied live The launcher initializes /etc/localtime from the accepted bundle the image API persisted, so a restarted Chromium starts in the session's timezone instead of the VM's startup zone. GET /browser/location reports a bundle applied only after the running Chromium confirms that generation with every component acknowledged, so a restarted or unreachable browser is never reported as applied while the upstream change is still being detected. The runtime e2e posts to /configure, the image's configure route. --- server/cmd/api/api/browser_location.go | 46 ++++++++++---- server/cmd/api/api/browser_location_test.go | 63 +++++++++++++++++++ server/cmd/chromium-launcher/main.go | 12 +++- server/cmd/chromium-launcher/main_test.go | 13 +++- .../e2e/e2e_browser_location_runtime_test.go | 2 +- server/lib/browserlocation/state.go | 31 +++++++++ 6 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 server/lib/browserlocation/state.go diff --git a/server/cmd/api/api/browser_location.go b/server/cmd/api/api/browser_location.go index 13dec28de..67fbf663e 100644 --- a/server/cmd/api/api/browser_location.go +++ b/server/cmd/api/api/browser_location.go @@ -21,10 +21,9 @@ import ( ) const ( - browserLocationApplyTimeout = 5 * time.Second - defaultBrowserLocationStatePath = "/run/kernel/browser-location.json" - trustedControlPlaneHeader = "X-Kernel-Trusted-Control-Plane" - trustedControlPlaneHeaderValue = "1" + browserLocationApplyTimeout = 5 * time.Second + trustedControlPlaneHeader = "X-Kernel-Trusted-Control-Plane" + trustedControlPlaneHeaderValue = "1" ) type browserLocationBundle struct { @@ -310,10 +309,7 @@ func rewriteLocaltime(timezone string) error { } func (s *ApiService) browserLocationStatePath() string { - if value := strings.TrimSpace(os.Getenv("KERNEL_BROWSER_LOCATION_STATE_PATH")); value != "" { - return value - } - return defaultBrowserLocationStatePath + return browserlocation.StatePath() } func (s *ApiService) persistBrowserLocationState(state browserLocationDurableState) error { @@ -479,10 +475,38 @@ func (s *ApiService) ResetBrowserLocationHTTP(w http.ResponseWriter, r *http.Req w.WriteHeader(http.StatusAccepted) } -// GetBrowserLocationHTTP exposes accepted/applied component state for internal reconciliation. -func (s *ApiService) GetBrowserLocationHTTP(w http.ResponseWriter, _ *http.Request) { +// GetBrowserLocationHTTP exposes accepted/applied component state for internal +// reconciliation. Applied is confirmed against the running Chromium, so a +// restarted or unreachable browser is never reported as applied. +func (s *ApiService) GetBrowserLocationHTTP(w http.ResponseWriter, r *http.Request) { + status := s.browserLocationSnapshot() + if status.Applied != nil { + generation, current := s.browserLocationGeneration(*status.Applied) + if !current || !s.browserLocationLive(r.Context(), *status.Applied, generation) { + status.Applied = nil + status.Components = browserLocationComponents{} + } + } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(s.browserLocationSnapshot()) + _ = json.NewEncoder(w).Encode(status) +} + +// browserLocationLive reports whether the running Chromium holds bundle at +// generation with every component acknowledged. +func (s *ApiService) browserLocationLive(ctx context.Context, bundle browserLocationBundle, generation uint64) bool { + if s.upstreamMgr == nil { + return false + } + var observed cdpclient.BrowserLocation + err := s.withCDPClientTimeout(ctx, 500*time.Millisecond, func(cdpCtx context.Context, client *cdpclient.Client) error { + var err error + observed, err = client.GetBrowserLocation(cdpCtx) + return err + }) + return err == nil && observed.Generation == generation && + observed.Locale == bundle.Locale && observed.AcceptLanguages == strings.Join(bundle.Languages, ",") && + observed.TimeZone == bundle.TimeZone && observed.TimeZoneConverged && + observed.RenderersConverged && observed.NetworkContextsConverged } func browserLocationBundlesEqual(a, b browserLocationBundle) bool { diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index b71783598..ca7865177 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "fmt" "log/slog" "net/http" @@ -9,9 +10,12 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "testing" "time" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -212,3 +216,62 @@ func TestBrowserLocationPersistenceFailureDoesNotChangeEpoch(t *testing.T) { require.Error(t, err) assert.Empty(t, service.browserLocationSnapshot().ActiveEpoch) } + +func TestGetBrowserLocationHTTPConfirmsAppliedWithChromium(t *testing.T) { + service := newBrowserLocationStateService(t) + bundle := testLocationBundle("lease-a", 1) + require.NoError(t, service.resetBrowserLocation("", bundle)) + service.browserLocationMu.Lock() + applied := bundle + service.browserLocation.applied = &applied + service.browserLocation.components = browserLocationComponents{TimeZone: true, Browser: true, Renderers: true, NetworkContexts: true} + service.browserLocationMu.Unlock() + + // Chromium reports this generation until it restarts and reports none. + var chromiumGeneration atomic.Uint64 + chromiumGeneration.Store(service.browserLocation.browserGeneration) + chromium := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, nil) + if err != nil { + return + } + defer conn.CloseNow() + for { + var command struct { + ID int `json:"id"` + } + if wsjson.Read(r.Context(), conn, &command) != nil { + return + } + _ = wsjson.Write(r.Context(), conn, map[string]any{"id": command.ID, "result": map[string]any{ + "generation": chromiumGeneration.Load(), "locale": "en-US", "acceptLanguages": "en-US,en", "timezone": "UTC", + "renderersConverged": true, "timezoneConverged": true, "networkContextsConverged": true, + }}) + } + })) + defer chromium.Close() + + logPath := filepath.Join(t.TempDir(), "chromium.log") + wsURL := "ws" + strings.TrimPrefix(chromium.URL, "http") + "/devtools/browser/a" + require.NoError(t, os.WriteFile(logPath, []byte("DevTools listening on "+wsURL+"\n"), 0o600)) + manager := devtoolsproxy.NewUpstreamManager(logPath, slog.New(slog.DiscardHandler)) + manager.Start(service.lifecycleCtx) + service.upstreamMgr = manager + require.Eventually(t, func() bool { return manager.Current() == wsURL }, 3*time.Second, 10*time.Millisecond) + + status := func() browserLocationStatus { + response := httptest.NewRecorder() + service.GetBrowserLocationHTTP(response, httptest.NewRequest(http.MethodGet, "/browser/location", nil)) + var got browserLocationStatus + require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got)) + return got + } + got := status() + require.NotNil(t, got.Applied) + assert.True(t, got.Components.Renderers) + + chromiumGeneration.Store(0) + got = status() + assert.Nil(t, got.Applied, "a restarted Chromium must not be reported as applied") + assert.Equal(t, browserLocationComponents{}, got.Components) +} diff --git a/server/cmd/chromium-launcher/main.go b/server/cmd/chromium-launcher/main.go index 0f8ee1d21..b425783ed 100644 --- a/server/cmd/chromium-launcher/main.go +++ b/server/cmd/chromium-launcher/main.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/kernel/kernel-images/server/lib/browserlocation" "github.com/kernel/kernel-images/server/lib/chromiumflags" "github.com/kernel/kernel-images/server/lib/x11" ) @@ -35,7 +36,7 @@ func main() { runtimeFlagsPath := flag.String("runtime-flags", "/chromium/flags", "Path to runtime flags overlay file") flag.Parse() - if err := applyStartupTimezone(startupTimezone(os.Getenv)); err != nil { + if err := applyStartupTimezone(startupTimezone(os.Getenv, os.ReadFile)); err != nil { fmt.Fprintf(os.Stderr, "failed to initialize browser timezone: %v\n", err) os.Exit(1) } @@ -259,7 +260,14 @@ func withoutEnvironmentVariable(env []string, key string) []string { return out } -func startupTimezone(getenv func(string) string) string { +// startupTimezone prefers the bundle the image API accepted, so a restarted +// Chromium starts in the session's current timezone rather than the VM's. +func startupTimezone(getenv func(string) string, readFile func(string) ([]byte, error)) string { + if data, err := readFile(browserlocation.StatePath()); err == nil { + if timezone := browserlocation.AcceptedTimeZone(data); timezone != "" { + return timezone + } + } if timezone := getenv("KERNEL_BROWSER_TIMEZONE"); timezone != "" { return timezone } diff --git a/server/cmd/chromium-launcher/main_test.go b/server/cmd/chromium-launcher/main_test.go index dd5ec735d..9b8a3d17d 100644 --- a/server/cmd/chromium-launcher/main_test.go +++ b/server/cmd/chromium-launcher/main_test.go @@ -105,15 +105,22 @@ func TestWithoutEnvironmentVariable(t *testing.T) { func TestStartupTimezone(t *testing.T) { values := map[string]string{"TZ": "America/Chicago", "KERNEL_BROWSER_TIMEZONE": "Europe/Berlin"} getenv := func(key string) string { return values[key] } - if got := startupTimezone(getenv); got != "Europe/Berlin" { + state := []byte(`{"active_epoch":"lease-a","accepted":{"epoch":"lease-a","generation":2,"timezone":"Asia/Singapore"}}`) + readState := func(string) ([]byte, error) { return state, nil } + noState := func(string) ([]byte, error) { return nil, os.ErrNotExist } + + if got := startupTimezone(getenv, readState); got != "Asia/Singapore" { + t.Fatalf("restart must keep the accepted timezone, got %s", got) + } + if got := startupTimezone(getenv, noState); got != "Europe/Berlin" { t.Fatalf("unexpected timezone: %s", got) } delete(values, "KERNEL_BROWSER_TIMEZONE") - if got := startupTimezone(getenv); got != "America/Chicago" { + if got := startupTimezone(getenv, noState); got != "America/Chicago" { t.Fatalf("unexpected fallback timezone: %s", got) } delete(values, "TZ") - if got := startupTimezone(getenv); got != "" { + if got := startupTimezone(getenv, noState); got != "" { t.Fatalf("unexpected empty timezone: %s", got) } } diff --git a/server/e2e/e2e_browser_location_runtime_test.go b/server/e2e/e2e_browser_location_runtime_test.go index ad823df30..cc883de29 100644 --- a/server/e2e/e2e_browser_location_runtime_test.go +++ b/server/e2e/e2e_browser_location_runtime_test.go @@ -269,7 +269,7 @@ func configureLocation(ctx context.Context, t *testing.T, c *TestContainer, bund writer := multipart.NewWriter(&body) require.NoError(t, writer.WriteField("browser_location", string(payload))) require.NoError(t, writer.Close()) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.APIBaseURL()+"/chromium/configure", &body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.APIBaseURL()+"/configure", &body) require.NoError(t, err) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) diff --git a/server/lib/browserlocation/state.go b/server/lib/browserlocation/state.go new file mode 100644 index 000000000..166cddc8f --- /dev/null +++ b/server/lib/browserlocation/state.go @@ -0,0 +1,31 @@ +package browserlocation + +import ( + "encoding/json" + "os" + "strings" +) + +const defaultStatePath = "/run/kernel/browser-location.json" + +// StatePath is where the image API persists the accepted location bundle. +func StatePath() string { + if value := strings.TrimSpace(os.Getenv("KERNEL_BROWSER_LOCATION_STATE_PATH")); value != "" { + return value + } + return defaultStatePath +} + +// AcceptedTimeZone returns the timezone of the persisted accepted bundle, or +// "" when none has been accepted. +func AcceptedTimeZone(data []byte) string { + var state struct { + Accepted *struct { + TimeZone string `json:"timezone"` + } `json:"accepted"` + } + if json.Unmarshal(data, &state) != nil || state.Accepted == nil { + return "" + } + return state.Accepted.TimeZone +} From 76e7bfb94b583907b01aa601f60b18ae7d68c140 Mon Sep 17 00:00:00 2001 From: tnsardesai <18272584+tnsardesai@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:08:06 +0000 Subject: [PATCH 11/11] Document browser_location configure failures and regenerate the API --- server/cmd/api/api/browser_location_test.go | 16 + server/cmd/api/api/chromium_configure_test.go | 15 + server/go.mod | 2 +- server/lib/oapi/oapi.go | 1434 +++++++++-------- server/openapi.yaml | 1 + 5 files changed, 756 insertions(+), 712 deletions(-) diff --git a/server/cmd/api/api/browser_location_test.go b/server/cmd/api/api/browser_location_test.go index ca7865177..83097ac05 100644 --- a/server/cmd/api/api/browser_location_test.go +++ b/server/cmd/api/api/browser_location_test.go @@ -17,6 +17,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" + "github.com/kernel/kernel-images/server/lib/oapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -217,6 +218,21 @@ func TestBrowserLocationPersistenceFailureDoesNotChangeEpoch(t *testing.T) { assert.Empty(t, service.browserLocationSnapshot().ActiveEpoch) } +func TestChromiumConfigureLocationPersistenceFailureReportsValidStep(t *testing.T) { + service := newBrowserLocationStateService(t) + require.NoError(t, service.resetBrowserLocation("", testLocationBundle("lease-a", 1))) + t.Setenv("KERNEL_BROWSER_LOCATION_STATE_PATH", "/proc/kernel-browser-location-state") + bundle := testLocationBundle("lease-a", 2) + + resp := service.applyBrowserLocationConfig(context.Background(), &chromiumConfigureState{browserLocation: &bundle}) + + failure, ok := resp.(oapi.ChromiumConfigure500JSONResponse) + require.True(t, ok, "expected 500 response, got %T", resp) + require.NotNil(t, failure.Step) + assert.Equal(t, oapi.BrowserLocation, *failure.Step) + assert.True(t, failure.Step.Valid()) +} + func TestGetBrowserLocationHTTPConfirmsAppliedWithChromium(t *testing.T) { service := newBrowserLocationStateService(t) bundle := testLocationBundle("lease-a", 1) diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go index 607acafa4..2cd8a1f03 100644 --- a/server/cmd/api/api/chromium_configure_test.go +++ b/server/cmd/api/api/chromium_configure_test.go @@ -30,6 +30,21 @@ func TestPoliciesContentNonEmpty(t *testing.T) { require.True(t, policiesContentNonEmpty(&real)) } +func TestChromiumConfigureStepsAreValidErrorSteps(t *testing.T) { + for _, step := range []chromiumConfigureStep{ + chromiumConfigureStepStop, + chromiumConfigureStepStart, + chromiumConfigureStepPolicies, + chromiumConfigureStepExtensions, + chromiumConfigureStepDisplay, + chromiumConfigureStepFlags, + chromiumConfigureStepProfile, + chromiumConfigureStepLocation, + } { + require.True(t, oapi.ChromiumConfigureErrorStep(step).Valid(), step) + } +} + func TestChromiumConfigureModeFor(t *testing.T) { stringPtr := func(value string) *string { return &value } diff --git a/server/go.mod b/server/go.mod index 8d55fa813..4e0c03dfc 100644 --- a/server/go.mod +++ b/server/go.mod @@ -37,6 +37,7 @@ require ( golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 + golang.org/x/text v0.40.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -114,7 +115,6 @@ require ( golang.org/x/crypto v0.54.0 // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.57.0 // indirect - golang.org/x/text v0.40.0 // indirect golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 22194a0c1..f064abbfb 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -2632,18 +2632,21 @@ func (e ChromiumConfigureErrorPhase) Valid() bool { // Defines values for ChromiumConfigureErrorStep. const ( - ChromePolicies ChromiumConfigureErrorStep = "chrome_policies" - ChromiumFlags ChromiumConfigureErrorStep = "chromium_flags" - Display ChromiumConfigureErrorStep = "display" - Extensions ChromiumConfigureErrorStep = "extensions" - Profile ChromiumConfigureErrorStep = "profile" - StartChromium ChromiumConfigureErrorStep = "start_chromium" - StopChromium ChromiumConfigureErrorStep = "stop_chromium" + BrowserLocation ChromiumConfigureErrorStep = "browser_location" + ChromePolicies ChromiumConfigureErrorStep = "chrome_policies" + ChromiumFlags ChromiumConfigureErrorStep = "chromium_flags" + Display ChromiumConfigureErrorStep = "display" + Extensions ChromiumConfigureErrorStep = "extensions" + Profile ChromiumConfigureErrorStep = "profile" + StartChromium ChromiumConfigureErrorStep = "start_chromium" + StopChromium ChromiumConfigureErrorStep = "stop_chromium" ) // Valid indicates whether the value is a known member of the ChromiumConfigureErrorStep enum. func (e ChromiumConfigureErrorStep) Valid() bool { switch e { + case BrowserLocation: + return true case ChromePolicies: return true case ChromiumFlags: @@ -7122,6 +7125,9 @@ type PatchChromiumPoliciesJSONBody map[string]interface{} // ChromiumConfigureMultipartBody defines parameters for ChromiumConfigure. type ChromiumConfigureMultipartBody struct { + // BrowserLocation UTF-8 JSON browser location bundle with epoch, generation, timezone, locale, and languages. Accepted asynchronously without restarting Chromium. + BrowserLocation *string `json:"browser_location,omitempty"` + // ChromePolicies UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. ChromePolicies *string `json:"chrome_policies,omitempty"` @@ -26639,710 +26645,716 @@ func (sh *strictHandler) GetWebMCPTools(w http.ResponseWriter, r *http.Request, // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 750e61cd7..81e240d55 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -6920,6 +6920,7 @@ components: - display - chromium_flags - profile + - browser_location RecorderInfo: type: object required: [id, isRecording]