diff --git a/server/Makefile b/server/Makefile index 303ea419..6f98d35b 100644 --- a/server/Makefile +++ b/server/Makefile @@ -36,7 +36,7 @@ test: @echo "" @echo "=== Running e2e tests (testcontainers — this may take a few minutes) ===" @echo "" - go test -v -race ./e2e/ + go test -v -race -timeout 120m ./e2e/ clean: @rm -rf $(BIN_DIR) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 0fbda144..a8040905 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -21,6 +21,12 @@ import ( var nameRegex = regexp.MustCompile(`^[A-Za-z0-9._-]{1,255}$`) +// extensionZipItem is a finalized name + temp zip path (caller removes temps). +type extensionZipItem struct { + zipTemp string + name string +} + // chromiumFlagsPath is the runtime flags file read by the chromium-launcher at startup. const chromiumFlagsPath = "/chromium/flags" @@ -130,52 +136,73 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "no extensions provided"}}, nil } - // Materialize uploads + extItems := make([]extensionZipItem, 0, len(items)) + for _, p := range items { + if !p.zipReceived || p.name == "" { + return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include zip_file and name"}}, nil + } + extItems = append(extItems, extensionZipItem{zipTemp: p.zipTemp, name: p.name}) + } + + reqMsg, err := s.applyExtensionZipItems(ctx, extItems) + if reqMsg != "" { + return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: reqMsg}}, nil + } + if err != nil { + return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}}, nil + } + + // Restart Chromium and wait for DevTools to be ready + if err := s.restartChromiumAndWait(ctx, "extension upload"); err != nil { + return oapi.UploadExtensionsAndRestart500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, + }, nil + } + + log.Info("devtools ready", "elapsed", time.Since(start).String()) + return oapi.UploadExtensionsAndRestart201Response{}, nil +} + +// applyExtensionZipItems applies name+zipTemp extension pairs (merge flags for --load-extension). +// On validation errors returns (reqMsg, nil); on internal errors returns ("", err). +func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensionZipItem) (reqMsg string, err error) { + log := logger.FromContext(ctx) extBase := "/home/kernel/extensions" - // Fail early if any destination already exists for _, p := range items { dest := filepath.Join(extBase, p.name) if _, err := os.Stat(dest); err == nil { - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("extension name already exists: %s", p.name)}}, nil + return fmt.Sprintf("extension name already exists: %s", p.name), nil } else if !os.IsNotExist(err) { log.Error("failed to check extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to check extension dir"}}, nil + return "", fmt.Errorf("failed to check extension dir: %w", err) } } for _, p := range items { - if !p.zipReceived || p.name == "" { - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "each item must include zip_file and name"}}, nil - } dest := filepath.Join(extBase, p.name) if err := os.MkdirAll(dest, 0o755); err != nil { log.Error("failed to create extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to create extension dir"}}, nil + return "", fmt.Errorf("failed to create extension dir: %w", err) } if err := ziputil.Unzip(p.zipTemp, dest); err != nil { log.Error("failed to unzip zip file", "error", err) - return oapi.UploadExtensionsAndRestart400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid zip file"}}, nil + return "invalid zip file", nil } - // Rewrite update.xml URLs to match the extension name (directory name) - // This ensures URLs like /extensions/web-bot-auth/ become /extensions// updateXMLPath := filepath.Join(dest, "update.xml") if err := policy.RewriteUpdateXMLUrls(updateXMLPath, p.name); err != nil { log.Warn("failed to rewrite update.xml URLs", "error", err, "extension", p.name) - // continue since not all extensions require update.xml } if err := exec.Command("chown", "-R", "kernel:kernel", dest).Run(); err != nil { log.Error("failed to chown extension dir", "error", err) - return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to chown extension dir"}}, nil + return "", fmt.Errorf("failed to chown extension dir: %w", err) } log.Info("installed extension", "name", p.name) } - // Update enterprise policy for extensions that require it - // Track which extensions need --load-extension flags (those NOT using policy installation) var pathsNeedingFlags []string for _, p := range items { @@ -184,14 +211,11 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap manifestPath := filepath.Join(extensionPath, "manifest.json") updateXMLPath := filepath.Join(extensionPath, "update.xml") - // Check if this extension requires enterprise policy requiresEntPolicy, err := s.policy.RequiresEnterprisePolicy(manifestPath) if err != nil { log.Warn("failed to read manifest for policy check", "error", err, "extension", extensionName) - // Continue with requiresEntPolicy = false } - // Try to extract Chrome extension ID from update.xml chromeExtensionID := extensionName var extractionErr error if extractedID, err := policy.ExtractExtensionIDFromUpdateXML(updateXMLPath); err == nil { @@ -205,25 +229,17 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap if requiresEntPolicy { log.Info("extension requires enterprise policy", "name", extensionName) - // Validate that update.xml and .crx files are present for policy-installed extensions - // These files are required for ExtensionInstallForcelist to work hasUpdateXML := false hasCRX := false if _, err := os.Stat(updateXMLPath); err == nil { - // For policy extensions, update.xml must exist AND be parseable if extractionErr != nil { - return oapi.UploadExtensionsAndRestart400JSONResponse{ - BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: fmt.Sprintf("extension %s requires enterprise policy but update.xml is invalid: %v", extensionName, extractionErr), - }, - }, nil + return fmt.Sprintf("extension %s requires enterprise policy but update.xml is invalid: %v", extensionName, extractionErr), nil } hasUpdateXML = true log.Info("found update.xml in extension zip", "name", extensionName) } - // Look for any .crx file in the directory entries, err := os.ReadDir(extensionPath) if err == nil { for _, entry := range entries { @@ -235,7 +251,6 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } } - // If missing required files for ExtensionInstallForcelist, fall back to --load-extension if !hasUpdateXML || !hasCRX { log.Info("extension missing policy files, falling back to --load-extension", "name", extensionName, "hasUpdateXML", hasUpdateXML, "hasCRX", hasCRX) @@ -243,31 +258,17 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) } } else { - // Only add --load-extension flags for non-policy extensions pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) } - // Add to enterprise policy - // Pass both extensionName (for URL paths) and chromeExtensionID (for policy entries) if err := s.policy.AddExtension(extensionName, chromeExtensionID, extensionPath, requiresEntPolicy); err != nil { log.Error("failed to update enterprise policy", "error", err, "extension", extensionName) - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ - Message: fmt.Sprintf("failed to update enterprise policy for %s: %v", extensionName, err), - }, - }, nil + return "", fmt.Errorf("failed to update enterprise policy for %s: %w", extensionName, err) } log.Info("updated enterprise policy", "extension", extensionName, "chromeExtensionID", chromeExtensionID, "requiresEnterprisePolicy", requiresEntPolicy) } - // Build flags overlay file in /chromium/flags, merging with existing flags - // Only add --load-extension flags for extensions that don't use policy installation - // NOTE: We intentionally do NOT use --disable-extensions-except here because it causes - // Chrome to disable external providers (including the policy loader), which prevents - // enterprise policy extensions (ExtensionInstallForcelist) from being fetched and installed. - // See Chromium source: extension_service.cc - external providers are only created when - // extensions_enabled() returns true, which is false when --disable-extensions-except is used. var newTokens []string if len(pathsNeedingFlags) > 0 { newTokens = []string{ @@ -275,22 +276,11 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } } - // Merge and write flags if _, err := s.mergeAndWriteChromiumFlags(ctx, newTokens); err != nil { - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, - }, nil - } - - // Restart Chromium and wait for DevTools to be ready - if err := s.restartChromiumAndWait(ctx, "extension upload"); err != nil { - return oapi.UploadExtensionsAndRestart500JSONResponse{ - InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: err.Error()}, - }, nil + return "", err } - log.Info("devtools ready", "elapsed", time.Since(start).String()) - return oapi.UploadExtensionsAndRestart201Response{}, nil + return "", nil } // mergeAndWriteChromiumFlags reads existing flags, merges them with new flags, @@ -370,6 +360,60 @@ func (s *ApiService) restartChromiumAndWait(ctx context.Context, operation strin } } +const supervisorCtlConf = "/etc/supervisor/supervisord.conf" + +func supervisorctlArgv(verb string, prog string) []string { + return []string{"-c", supervisorCtlConf, verb, prog} +} + +// stopChromium runs supervisorctl stop chromium and waits for the command to complete. +func (s *ApiService) stopChromium(ctx context.Context) error { + log := logger.FromContext(ctx) + cmdCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) + defer cancel() + log.Info("stopping chromium via supervisorctl") + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("stop", "chromium")...).CombinedOutput() + if err != nil { + log.Error("failed to stop chromium", "error", err, "out", string(out)) + return fmt.Errorf("supervisorctl stop chromium failed: %w", err) + } + return nil +} + +// startChromiumAndWait launches chromium via supervisorctl start and waits for DevTools readiness. +func (s *ApiService) startChromiumAndWait(ctx context.Context, operation string) error { + log := logger.FromContext(ctx) + start := time.Now() + + updates, cancelSub := s.upstreamMgr.Subscribe() + defer cancelSub() + + errCh := make(chan error, 1) + log.Info("starting chromium via supervisorctl", "operation", operation) + go func() { + cmdCtx, cancelCmd := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) + defer cancelCmd() + out, err := exec.CommandContext(cmdCtx, "supervisorctl", supervisorctlArgv("start", "chromium")...).CombinedOutput() + if err != nil { + log.Error("failed to start chromium", "error", err, "out", string(out)) + errCh <- fmt.Errorf("supervisorctl start chromium failed: %w", err) + } + }() + + timeout := time.NewTimer(15 * time.Second) + defer timeout.Stop() + select { + case <-updates: + log.Info("devtools ready", "operation", operation, "elapsed", time.Since(start).String()) + return nil + case err := <-errCh: + return err + case <-timeout.C: + log.Info("devtools not ready in time", "operation", operation, "elapsed", time.Since(start).String()) + return fmt.Errorf("devtools not ready in time") + } +} + // PatchChromiumPolicies applies user-provided Chromium enterprise policy overrides // to policy.json, restarts Chromium, and waits for DevTools to be ready. func (s *ApiService) PatchChromiumPolicies(ctx context.Context, request oapi.PatchChromiumPoliciesRequestObject) (oapi.PatchChromiumPoliciesResponseObject, error) { diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go new file mode 100644 index 00000000..ad6a68a0 --- /dev/null +++ b/server/cmd/api/api/chromium_configure.go @@ -0,0 +1,623 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/url" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/kernel/kernel-images/server/lib/logger" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/kernel/kernel-images/server/lib/policy" + "github.com/kernel/kernel-images/server/lib/zstdutil" +) + +const userDataProfileDir = "/home/kernel/user-data" + +type chromiumConfigureState struct { + displayJSON *string + chromiumFlagsJSON *string + chromePoliciesJSON *string + + stripComponents int + + profileTemp string // temp archive path + hasProfile bool + + startURLRaw *string + + extItems []extensionZipItem // zipTemp paths; merged with chromiumCfgParseExtensions + + allTemps []string +} + +func (st *chromiumConfigureState) cleanup() { + for _, p := range st.allTemps { + _ = os.Remove(p) + } +} + +// ChromiumConfigure batched Chromium/session configuration plus optional navigation. +func (s *ApiService) ChromiumConfigure(ctx context.Context, request oapi.ChromiumConfigureRequestObject) (oapi.ChromiumConfigureResponseObject, error) { + start := time.Now() + + if request.Body == nil { + return cfg400("request body required"), nil + } + + st := &chromiumConfigureState{} + if msg := chromiumCfgParseMultipart(request.Body, st); msg != "" { + st.cleanup() + return cfg400(msg), nil + } + defer st.cleanup() + + if cfgActionables(st)+cfgHasStartURL(st.startURLRaw) == 0 { + return cfg400("no configuration fields provided"), nil + } + + needsStop := chromiumNeedsStopCycle(st) + + if needsStop { + if chromiumDisplayHasSizedRequest(st.displayJSON) { + stopped, stopErr := s.stopActiveRecordings(ctx) + if stopErr != nil { + return cfg500Configure(fmt.Sprintf("failed to stop recordings: %v", stopErr)), nil + } + if len(stopped) > 0 { + defer func() { + go s.startNewRecordingSegments(context.WithoutCancel(ctx), stopped) + }() + } + } + + logger.FromContext(ctx).Info("chromium configure (stop/start path)") + if err := s.stopChromium(ctx); err != nil { + return cfg500Configure(err.Error()), nil + } + + if st.hasProfile { + if err := chromiumApplyProfileArchive(st.profileTemp, st.stripComponents); err != nil { + return cfg500Configure(err.Error()), nil + } + } + + if chromiumDisplayHasSizedRequest(st.displayJSON) { + b, msgs := chromiumParseDisplayParts(st.displayJSON) + if msgs != "" { + return cfg400(msgs), nil + } + if b != nil { + if rr := chromiumDisplayApplyWhileStopped(ctx, s, b); rr != nil { + return rr, nil + } + } + } + + if msgs := chromiumApplyPolicies(ctx, s, st.chromePoliciesJSON); msgs != "" { + return policyDisposition(msgs), nil + } + + if reqMsgs, ierr := chromiumApplyExtensions(ctx, s, st.extItems); reqMsgs != "" { + return cfg400(reqMsgs), nil + } else if ierr != nil { + return cfg500Configure(ierr.Error()), nil + } + + if msgs := chromiumMergeFlagsRaw(ctx, s, st.chromiumFlagsJSON); msgs != "" { + if strings.HasPrefix(msgs, "bad:") { + return cfg400(strings.TrimPrefix(msgs, "bad:")), nil + } + return cfg500Configure(strings.TrimPrefix(msgs, "int:")), nil + } + + if err := s.startChromiumAndWait(ctx, "batched chromium configure"); err != nil { + return cfg500Configure(err.Error()), nil + } + } else { + if st.displayJSON != nil && strings.TrimSpace(*st.displayJSON) != "" { + body, msgs := chromiumParseDisplayParts(st.displayJSON) + if msgs != "" { + return cfg400(msgs), nil + } + if rr := chromiumRunPatchDisplay(ctx, s, body); rr != nil { + return rr, nil + } + } + } + + spec, msgs := chromiumStartURLSpec(st.startURLRaw) + if msgs != "" { + return cfg400(msgs), nil + } + if spec.needsNav { + if err := chromiumDoNavigate(ctx, s, spec); err != nil { + return cfg500Navigate(err.Error()), nil + } + } + + logger.FromContext(ctx).Info("chromium configure finished", "elapsed", time.Since(start).String()) + return oapi.ChromiumConfigure200JSONResponse{Ok: true}, nil +} + +type startURLParsed struct { + needsNav bool + url string + wait cdpclient.NavigateWaitUntil + timeout time.Duration +} + +func chromiumStartURLSpec(raw *string) (startURLParsed, string) { + var out startURLParsed + out.timeout = 45 * time.Second + out.wait = cdpclient.NavigateWaitLoad + if raw == nil || strings.TrimSpace(*raw) == "" { + return out, "" + } + s := strings.TrimSpace(*raw) + if strings.HasPrefix(s, "{") { + var v struct { + URL string `json:"url"` + WaitUntil string `json:"wait_until"` + Timeout *int `json:"timeout_sec,omitempty"` + } + if err := json.Unmarshal([]byte(s), &v); err != nil { + return out, "invalid start_url JSON" + } + if strings.TrimSpace(v.URL) == "" { + return out, "start_url JSON requires url" + } + switch strings.TrimSpace(strings.ToLower(v.WaitUntil)) { + case "", "load": + out.wait = cdpclient.NavigateWaitLoad + case "domcontentloaded": + out.wait = cdpclient.NavigateWaitDOMContentLoaded + default: + return out, "wait_until must be load or domcontentloaded" + } + out.url = strings.TrimSpace(v.URL) + if v.Timeout != nil && *v.Timeout > 0 { + out.timeout = time.Duration(*v.Timeout) * time.Second + } + } else { + out.url = s + } + if errMsgs := chromiumValidateNavigateURL(out.url); errMsgs != "" { + return out, errMsgs + } + out.needsNav = true + return out, "" +} + +func chromiumValidateNavigateURL(u string) string { + parsed, err := url.Parse(u) + if err != nil { + return "invalid start URL" + } + switch strings.ToLower(parsed.Scheme) { + case "https", "http", "about", "data", "chrome", "devtools": + default: + return fmt.Sprintf("unsupported URL scheme %q", parsed.Scheme) + } + return "" +} + +func chromiumDoNavigate(ctx context.Context, s *ApiService, spec startURLParsed) error { + upstream := s.upstreamMgr.Current() + if upstream == "" { + return fmt.Errorf("devtools upstream not available") + } + navCtx, cancel := context.WithTimeout(ctx, spec.timeout) + defer cancel() + return cdpclient.NavigateFirstPage(navCtx, upstream, spec.url, spec.wait) +} + +func chromiumNeedsStopCycle(st *chromiumConfigureState) bool { + return st.hasProfile || + len(st.extItems) > 0 || + policiesContentNonEmpty(st.chromePoliciesJSON) || + flagsContentNonEmpty(st.chromiumFlagsJSON) +} + +func policiesContentNonEmpty(s *string) bool { + if !policiesNonEmpty(s) { + return false + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(strings.TrimSpace(*s)), &m); err != nil { + return true + } + return len(m) > 0 +} + +func flagsContentNonEmpty(s *string) bool { + if !flagsNonEmpty(s) { + return false + } + var raw struct { + Flags []string `json:"flags"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(*s)), &raw); err != nil { + return true + } + return len(raw.Flags) > 0 +} + +func policiesNonEmpty(s *string) bool { + return s != nil && strings.TrimSpace(*s) != "" +} + +func flagsNonEmpty(s *string) bool { + return s != nil && strings.TrimSpace(*s) != "" +} + +func chromiumDisplayHasSizedRequest(displayJSON *string) bool { + if displayJSON == nil { + return false + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(*displayJSON), &raw); err != nil { + return false + } + w, ow := raw["width"] + h, oh := raw["height"] + if !ow || !oh { + return false + } + fw, wok := w.(float64) + fh, hok := h.(float64) + if wok && hok && fw > 0 && fh > 0 { + return true + } + iw, wok := w.(int) + ih, hok := h.(int) + return wok && hok && iw > 0 && ih > 0 +} + +func cfg400(msg string) oapi.ChromiumConfigure400JSONResponse { + return oapi.ChromiumConfigure400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: msg}, + } +} + +func cfg500Configure(msg string) oapi.ChromiumConfigure500JSONResponse { + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.ConfigurePhase, + Message: msg, + }) +} + +func cfg500Navigate(msg string) oapi.ChromiumConfigure500JSONResponse { + return oapi.ChromiumConfigure500JSONResponse(oapi.ChromiumConfigureError{ + Phase: oapi.NavigatePhase, + Message: msg, + }) +} + +func cfgActionables(st *chromiumConfigureState) int { + n := 0 + if policiesContentNonEmpty(st.chromePoliciesJSON) { + n++ + } + if flagsContentNonEmpty(st.chromiumFlagsJSON) { + n++ + } + if len(st.extItems) > 0 { + n++ + } + if st.hasProfile { + n++ + } + if chromiumDisplayHasSizedRequest(st.displayJSON) { + n++ + } + return n +} + +func cfgHasStartURL(s *string) int { + if s == nil || strings.TrimSpace(*s) == "" { + return 0 + } + return 1 +} + +func chromiumCfgParseMultipart(body interface{}, st *chromiumConfigureState) string { + mr, ok := any(body).(interface { + NextPart() (*multipart.Part, error) + }) + if !ok { + return "multipart reader not available" + } + + type pend struct { + zipTmp string + name string + gotZip bool + } + var cur *pend + + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + return "failed reading multipart" + } + switch name := part.FormName(); name { + case "display": + b, err := io.ReadAll(part) + if err != nil { + return "read display field" + } + v := strings.TrimSpace(string(b)) + st.displayJSON = &v + case "chromium_flags": + b, err := io.ReadAll(part) + if err != nil { + return "read chromium_flags field" + } + v := string(b) + st.chromiumFlagsJSON = &v + case "chrome_policies": + b, err := io.ReadAll(part) + if err != nil { + return "read chrome_policies field" + } + v := string(b) + st.chromePoliciesJSON = &v + case "strip_components": + b, err := io.ReadAll(part) + if err != nil { + return "read strip_components" + } + if n, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil && n >= 0 { + st.stripComponents = n + } + case "profile_archive": + tmp, err := os.CreateTemp("", "bcc-prof-*.tar.zst") + if err != nil { + return "temp profile_archive" + } + st.allTemps = append(st.allTemps, tmp.Name()) + if _, err := io.Copy(tmp, part); err != nil { + tmp.Close() + return "read profile_archive" + } + if err := tmp.Close(); err != nil { + return "finalize profile_archive" + } + st.profileTemp = tmp.Name() + st.hasProfile = true + case "start_url": + b, err := io.ReadAll(part) + if err != nil { + return "read start_url" + } + v := string(b) + st.startURLRaw = &v + case "extensions.zip_file": + if cur == nil { + cur = &pend{} + } + tmp, err := os.CreateTemp("", "bcc-ext-*.zip") + if err != nil { + return "temp extensions.zip_file" + } + st.allTemps = append(st.allTemps, tmp.Name()) + if _, err := io.Copy(tmp, part); err != nil { + tmp.Close() + return "read extensions.zip_file" + } + if err := tmp.Close(); err != nil { + return "close extensions.zip_file" + } + if cur.gotZip { + return "duplicate extensions.zip_file pair" + } + cur.zipTmp = tmp.Name() + cur.gotZip = true + case "extensions.name": + if cur == nil { + cur = &pend{} + } + b, err := io.ReadAll(part) + if err != nil { + return "read extensions.name" + } + nm := strings.TrimSpace(string(b)) + if nm == "" || !nameRegex.MatchString(nm) { + return "invalid extensions.name" + } + if cur.name != "" { + return "duplicate extensions.name in pair" + } + cur.name = nm + default: + return fmt.Sprintf("unknown form field %q", name) + } + if cur != nil && cur.gotZip && cur.name != "" { + st.extItems = append(st.extItems, extensionZipItem{zipTemp: cur.zipTmp, name: cur.name}) + cur = nil + } + } + if cur != nil && (!cur.gotZip || cur.name == "") { + return "each extension pair needs extensions.zip_file plus extensions.name" + } + return "" +} + +func chromiumApplyProfileArchive(profilePath string, strip int) error { + if err := os.RemoveAll(userDataProfileDir); err != nil { + return fmt.Errorf("clear user-data: %w", err) + } + if err := os.MkdirAll(userDataProfileDir, 0o755); err != nil { + return fmt.Errorf("mkdir user-data: %w", err) + } + f, err := os.Open(profilePath) + if err != nil { + return err + } + defer f.Close() + if err := zstdutil.UntarZstd(f, userDataProfileDir, strip); err != nil { + return fmt.Errorf("extract profile archive: %w", err) + } + out, err := exec.Command("chown", "-R", "kernel:kernel", userDataProfileDir).CombinedOutput() + if err != nil { + return fmt.Errorf("chown user-data: %w (%s)", err, string(out)) + } + return nil +} + +func chromiumParseDisplayParts(displayJSON *string) (*oapi.PatchDisplayJSONRequestBody, string) { + if displayJSON == nil { + return nil, "" + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(*displayJSON), &raw); err != nil { + return nil, "invalid display JSON" + } + if len(raw) == 0 { + return nil, "display payload empty" + } + blob, err := json.Marshal(raw) + if err != nil { + return nil, "invalid display marshal" + } + var body oapi.PatchDisplayJSONRequestBody + if err := json.Unmarshal(blob, &body); err != nil { + return nil, fmt.Sprintf("invalid display payload: %v", err) + } + return &body, "" +} + +func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, body *oapi.PatchDisplayRequest) oapi.ChromiumConfigureResponseObject { + if body.Width == nil || body.Height == nil { + return nil + } + w, h := *body.Width, *body.Height + if w <= 0 || h <= 0 { + return cfg400("display width and height must be positive") + } + mode := s.detectDisplayMode(ctx) + rr := 60 + if body.RefreshRate != nil { + rr = int(*body.RefreshRate) + } + if mode == "xvfb" { + s.xvfbResizeMu.Lock() + err := s.resizeXvfb(ctx, w, h) + s.xvfbResizeMu.Unlock() + if err != nil { + return cfg500Configure(err.Error()) + } + s.clearViewportOverride() + return nil + } + var err error + if s.isNekoEnabled() { + err = s.setResolutionViaNeko(ctx, w, h, rr) + } else { + err = s.setResolutionXorgViaXrandr(ctx, w, h, rr, false) + } + if err != nil { + return cfg500Configure(err.Error()) + } + return nil +} + +func chromiumRunPatchDisplay(ctx context.Context, s *ApiService, body *oapi.PatchDisplayJSONRequestBody) oapi.ChromiumConfigureResponseObject { + resp, err := s.PatchDisplay(ctx, oapi.PatchDisplayRequestObject{Body: body}) + if err != nil { + return cfg500Configure(err.Error()) + } + switch r := resp.(type) { + case oapi.PatchDisplay200JSONResponse: + return nil + case oapi.PatchDisplay400JSONResponse: + return cfg400(r.Message) + case oapi.PatchDisplay409JSONResponse: + return oapi.ChromiumConfigure409JSONResponse{ConflictErrorJSONResponse: r.ConflictErrorJSONResponse} + case oapi.PatchDisplay500JSONResponse: + return cfg500Configure(r.Message) + default: + return cfg500Configure("unexpected PatchDisplay response") + } +} + +func chromiumApplyPolicies(ctx context.Context, s *ApiService, raw *string) string { + if raw == nil || strings.TrimSpace(*raw) == "" { + return "" + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(*raw), &m); err != nil { + return "bad:invalid chrome_policies JSON" + } + if len(m) == 0 { + return "" + } + overrides, err := policy.NewChromiumPolicyOverrides(m) + if err != nil { + if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { + return "bad:" + err.Error() + } + return "int:" + err.Error() + } + if err := s.policy.ApplyOverrides(overrides); err != nil { + if strings.Contains(err.Error(), "cannot be overridden") || strings.Contains(err.Error(), "invalid chromium policy overrides") { + return "bad:" + err.Error() + } + return "int:" + err.Error() + } + return "" +} + +func policyDisposition(msgs string) oapi.ChromiumConfigureResponseObject { + if strings.HasPrefix(msgs, "bad:") { + return cfg400(strings.TrimPrefix(msgs, "bad:")) + } + return cfg500Configure(strings.TrimPrefix(msgs, "int:")) +} + +func chromiumApplyExtensions(ctx context.Context, s *ApiService, items []extensionZipItem) (string, error) { + if len(items) == 0 { + return "", nil + } + return s.applyExtensionZipItems(ctx, items) +} + +func chromiumMergeFlagsRaw(ctx context.Context, s *ApiService, raw *string) string { + if raw == nil || strings.TrimSpace(*raw) == "" { + return "" + } + var body struct { + Flags []string `json:"flags"` + } + if err := json.Unmarshal([]byte(*raw), &body); err != nil { + return "bad:invalid chromium_flags JSON" + } + if len(body.Flags) == 0 { + return "bad:chromium_flags requires at least one flag" + } + for _, flag := range body.Flags { + t := strings.TrimSpace(flag) + if t == "" { + return "bad:empty flag in chromium_flags" + } + if !strings.HasPrefix(t, "--") { + return fmt.Sprintf("bad:invalid flag format: %s (must start with --)", flag) + } + } + if _, err := s.mergeAndWriteChromiumFlags(ctx, body.Flags); err != nil { + return "int:" + err.Error() + } + return "" +} diff --git a/server/cmd/api/api/chromium_configure_test.go b/server/cmd/api/api/chromium_configure_test.go new file mode 100644 index 00000000..f02fd8c7 --- /dev/null +++ b/server/cmd/api/api/chromium_configure_test.go @@ -0,0 +1,76 @@ +package api + +import ( + "bytes" + "mime/multipart" + "strings" + "testing" + "time" + + "github.com/kernel/kernel-images/server/lib/cdpclient" + "github.com/stretchr/testify/require" +) + +func TestFlagsContentNonEmpty(t *testing.T) { + emptyArr := `{}` + fl := `{"flags":[]}` + real := `{"flags":["--kiosk"]}` + require.False(t, flagsContentNonEmpty(&emptyArr)) + require.False(t, flagsContentNonEmpty(&fl)) + require.True(t, flagsContentNonEmpty(&real)) +} + +func TestPoliciesContentNonEmpty(t *testing.T) { + emptyObj := `{}` + real := `{"DefaultCookiesSetting": 1}` + require.False(t, policiesContentNonEmpty(&emptyObj)) + require.True(t, policiesContentNonEmpty(&real)) +} + +func TestChromiumStartURLSpec_plainAndJSON(t *testing.T) { + plain := "https://example.com/" + out, errs := chromiumStartURLSpec(&plain) + require.Empty(t, errs) + require.True(t, out.needsNav) + require.Equal(t, plain, out.url) + require.Equal(t, 45*time.Second, out.timeout) + require.Equal(t, cdpclient.NavigateWaitLoad, out.wait) + + raw := `{"url":"https://a.test/x","wait_until":"domcontentloaded","timeout_sec":12}` + out, errs = chromiumStartURLSpec(&raw) + require.Empty(t, errs) + require.True(t, out.needsNav) + require.Equal(t, "https://a.test/x", out.url) + require.Equal(t, 12*time.Second, out.timeout) + require.Equal(t, cdpclient.NavigateWaitDOMContentLoaded, out.wait) + + badScheme := "file:///etc/passwd" + _, errs = chromiumStartURLSpec(&badScheme) + require.NotEmpty(t, errs) + + badWait := `{"url":"https://x.example","wait_until":"networkidle"}` + _, errs = chromiumStartURLSpec(&badWait) + require.NotEmpty(t, errs) +} + +func TestChromiumCfgParseMultipart(t *testing.T) { + buf := bytes.NewBuffer(nil) + w := multipart.NewWriter(buf) + + require.NoError(t, w.WriteField("chrome_policies", `{"HttpsUpgradesEnabled":false}`)) + require.NoError(t, w.WriteField("strip_components", "2")) + require.NoError(t, w.WriteField("start_url", "https://kernel.example/route")) + + require.NoError(t, w.Close()) + + br := multipart.NewReader(buf, w.Boundary()) + st := &chromiumConfigureState{} + msg := chromiumCfgParseMultipart(br, st) + defer st.cleanup() + require.Empty(t, msg) + + require.True(t, policiesContentNonEmpty(st.chromePoliciesJSON)) + require.Equal(t, 2, st.stripComponents) + require.NotNil(t, st.startURLRaw) + require.Equal(t, "https://kernel.example/route", strings.TrimSpace(*st.startURLRaw)) +} diff --git a/server/e2e/e2e_chromium_configure_powerset_test.go b/server/e2e/e2e_chromium_configure_powerset_test.go new file mode 100644 index 00000000..c58352c9 --- /dev/null +++ b/server/e2e/e2e_chromium_configure_powerset_test.go @@ -0,0 +1,199 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/stretchr/testify/require" +) + +const ( + matDisplay = 1 << iota + matPolicy + matKioskFlags + matExtension + matStartURL + + matMaxBitmask = matDisplay | matPolicy | matKioskFlags | matExtension | matStartURL // 31 +) + +// TestChromiumConfigureMultipartPowerset runs sequential subtests covering every non-empty combination +// of multipart parts (display, chrome_policies, chromium_flags kiosk, extensions, start_url). +// Run after: images/chromium-headless/build-docker.sh (default image onkernel/chromium-headless-test:latest). +func TestChromiumConfigureMultipartPowerset(t *testing.T) { + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + extDir, err := filepath.Abs("test-extension") + require.NoError(t, err) + extZip, err := zipDirToBytes(extDir) + require.NoError(t, err) + + for bits := 1; bits <= matMaxBitmask; bits++ { + bits := bits + t.Run(chromiumConfigurePowersetLabel(bits), func(t *testing.T) { + + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{ + "WIDTH": "1024", + "HEIGHT": "768", + }, + }), "failed to start container") + defer func() { _ = c.Stop(context.WithoutCancel(ctx)) }() + + require.NoError(t, c.WaitReady(ctx)) + + var body bytes.Buffer + w := multipart.NewWriter(&body) + require.NoError(t, chromiumConfigurePowersetPopulate(t, w, bits, extZip)) + require.NoError(t, w.Close()) + + client, err := c.APIClient() + require.NoError(t, err) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(bytes.NewReader(body.Bytes()))) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, rsp.StatusCode(), + "bits=%02x unexpected status=%s body=%s", bits, rsp.Status(), string(rsp.Body)) + require.NotNil(t, rsp.JSON200, "want ok JSON") + require.True(t, rsp.JSON200.Ok) + }) + } +} + +func chromiumConfigurePowersetLabel(bits int) string { + var p []string + if bits&matDisplay != 0 { + p = append(p, "display") + } + if bits&matPolicy != 0 { + p = append(p, "policy") + } + if bits&matKioskFlags != 0 { + p = append(p, "kiosk") + } + if bits&matExtension != 0 { + p = append(p, "ext") + } + if bits&matStartURL != 0 { + p = append(p, "nav") + } + return strings.Join(p, "+") +} + +func chromiumConfigurePowersetPopulate(t *testing.T, w *multipart.Writer, bits int, extZip []byte) error { + t.Helper() + + if bits&matDisplay != 0 { + restart := true + requireIdle := true + disp := instanceoapi.PatchDisplayJSONRequestBody{ + Width: intPtr(1280), + Height: intPtr(720), + RestartChromium: &restart, + RequireIdle: &requireIdle, + } + blob, err := json.Marshal(disp) + require.NoError(t, err) + if err := w.WriteField("display", string(blob)); err != nil { + return err + } + } + + if bits&matPolicy != 0 { + // QuicAllowed false is benign and allowed by server policy registry / overrides validation. + pol := map[string]interface{}{"QuicAllowed": false} + blob, err := json.Marshal(pol) + require.NoError(t, err) + if err := w.WriteField("chrome_policies", string(blob)); err != nil { + return err + } + } + + if bits&matKioskFlags != 0 { + fl := instanceoapi.PatchChromiumFlagsJSONBody{Flags: []string{"--kiosk"}} + blob, err := json.Marshal(fl) + require.NoError(t, err) + if err := w.WriteField("chromium_flags", string(blob)); err != nil { + return err + } + } + + if bits&matExtension != 0 { + part, err := w.CreateFormFile("extensions.zip_file", "powerset-ext.zip") + if err != nil { + return err + } + if _, err := io.Copy(part, bytes.NewReader(extZip)); err != nil { + return err + } + if err := w.WriteField("extensions.name", "powerset"); err != nil { + return err + } + } + + if bits&matStartURL != 0 { + if err := w.WriteField("start_url", `https://example.com/`); err != nil { + return err + } + } + return nil +} + +func intPtr(i int) *int { return &i } + +// TestChromiumConfigureMultistartJSONObject exercises JSON start_url variant (wait_until JSON). +func TestChromiumConfigureStartURLJSONObject(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{"WIDTH": "1024", "HEIGHT": "768"}, + })) + defer c.Stop(ctx) + require.NoError(t, c.WaitReady(ctx)) + + payload := map[string]string{ + "url": "https://example.com/", + "wait_until": "domcontentloaded", + } + raw, err := json.Marshal(payload) + require.NoError(t, err) + + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + require.NoError(t, mw.WriteField("start_url", string(raw))) + require.NoError(t, mw.Close()) + + client, err := c.APIClient() + require.NoError(t, err) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, mw.FormDataContentType(), io.NopCloser(bytes.NewReader(buf.Bytes()))) + require.NoError(t, err) + require.Equal(t, http.StatusOK, rsp.StatusCode(), "%s", string(rsp.Body)) + require.True(t, rsp.JSON200.Ok) +} diff --git a/server/e2e/e2e_chromium_configure_test.go b/server/e2e/e2e_chromium_configure_test.go new file mode 100644 index 00000000..6d4811f2 --- /dev/null +++ b/server/e2e/e2e_chromium_configure_test.go @@ -0,0 +1,51 @@ +package e2e + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestChromiumConfigureStartURLBare(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{ + "WIDTH": "1024", + "HEIGHT": "768", + }, + }), "failed to start container") + defer c.Stop(ctx) + + require.NoError(t, c.WaitReady(ctx)) + + client, err := c.APIClient() + require.NoError(t, err) + + var buf bytes.Buffer + w := multipart.NewWriter(&buf) + require.NoError(t, w.WriteField("start_url", `https://example.com/`)) + require.NoError(t, w.Close()) + + rsp, err := client.ChromiumConfigureWithBodyWithResponse(ctx, w.FormDataContentType(), io.NopCloser(&buf)) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status=%s body=%s", rsp.Status(), string(rsp.Body)) + require.NotNil(t, rsp.JSON200, "want ok json") + require.True(t, rsp.JSON200.Ok) +} diff --git a/server/lib/cdpclient/cdpclient.go b/server/lib/cdpclient/cdpclient.go index eff95a00..6692db88 100644 --- a/server/lib/cdpclient/cdpclient.go +++ b/server/lib/cdpclient/cdpclient.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sync/atomic" + "time" "github.com/coder/websocket" ) @@ -98,6 +99,131 @@ func (c *Client) send(ctx context.Context, method string, params any, sessionID } } +// NavigateWaitUntil controls how long NavigateFirstPage waits after Page.navigate. +type NavigateWaitUntil string + +const ( + NavigateWaitLoad NavigateWaitUntil = "load" + NavigateWaitDOMContentLoaded NavigateWaitUntil = "domcontentloaded" +) + +// NavigateFirstPage attaches to the first page target, navigates to url, and optionally +// waits for load or DOMContentLoaded. Uses a browser-level WebSocket (flattened session). +func NavigateFirstPage(ctx context.Context, devtoolsURL, url string, waitUntil NavigateWaitUntil) error { + c, err := Dial(ctx, devtoolsURL) + if err != nil { + return fmt.Errorf("dial devtools: %w", err) + } + defer c.Close() + + targetsResult, err := c.send(ctx, "Target.getTargets", nil, "") + if err != nil { + return fmt.Errorf("Target.getTargets: %w", err) + } + + var targets struct { + TargetInfos []struct { + TargetID string `json:"targetId"` + Type string `json:"type"` + } `json:"targetInfos"` + } + if err := json.Unmarshal(targetsResult, &targets); err != nil { + return fmt.Errorf("unmarshal targets: %w", err) + } + + var pageTargetID string + for _, t := range targets.TargetInfos { + if t.Type == "page" { + pageTargetID = t.TargetID + break + } + } + if pageTargetID == "" { + return fmt.Errorf("no page target found") + } + + attachResult, err := c.send(ctx, "Target.attachToTarget", map[string]any{ + "targetId": pageTargetID, + "flatten": true, + }, "") + if err != nil { + return fmt.Errorf("Target.attachToTarget: %w", err) + } + + var attach struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(attachResult, &attach); err != nil { + return fmt.Errorf("unmarshal attach: %w", err) + } + sess := attach.SessionID + defer func() { + detachCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, _ = c.send(detachCtx, "Target.detachFromTarget", map[string]any{ + "sessionId": sess, + }, "") + }() + + if _, err := c.send(ctx, "Page.enable", map[string]any{}, sess); err != nil { + return fmt.Errorf("Page.enable: %w", err) + } + + wantEvents := map[string]struct{}{} + switch waitUntil { + case NavigateWaitDOMContentLoaded: + wantEvents["Page.domContentEventFired"] = struct{}{} + case NavigateWaitLoad, "": + wantEvents["Page.loadEventFired"] = struct{}{} + default: + return fmt.Errorf("unsupported wait_until: %q", waitUntil) + } + + // Only one goroutine may read from the WebSocket — never overlap send()'s Read + // loop with waitForPageEvents. + if _, err := c.send(ctx, "Page.navigate", map[string]any{"url": url}, sess); err != nil { + return fmt.Errorf("Page.navigate: %w", err) + } + if err := c.waitForPageEvents(ctx, sess, wantEvents); err != nil { + return err + } + + return nil +} + +func (c *Client) waitForPageEvents(ctx context.Context, sessionID string, want map[string]struct{}) error { + for { + select { + case <-ctx.Done(): + return fmt.Errorf("wait for navigation event: %w", ctx.Err()) + default: + } + + _, msg, err := c.conn.Read(ctx) + if err != nil { + return fmt.Errorf("read cdp: %w", err) + } + + var envelope struct { + Method string `json:"method"` + SessionID string `json:"sessionId"` + Params json.RawMessage `json:"params"` + } + if err := json.Unmarshal(msg, &envelope); err != nil { + continue + } + if envelope.Method == "" { + continue + } + if envelope.SessionID != sessionID { + continue + } + if _, ok := want[envelope.Method]; ok { + return nil + } + } +} + // SetDeviceMetricsOverride sets the viewport dimensions on the first page // target found in the browser. It attaches to the target with a flattened // session, sends Emulation.setDeviceMetricsOverride, then detaches. diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 2269d704..4afa5326 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -437,6 +437,24 @@ func (e BrowserTargetType) Valid() bool { } } +// Defines values for ChromiumConfigureErrorPhase. +const ( + ConfigurePhase ChromiumConfigureErrorPhase = "configure_phase" + NavigatePhase ChromiumConfigureErrorPhase = "navigate_phase" +) + +// Valid indicates whether the value is a known member of the ChromiumConfigureErrorPhase enum. +func (e ChromiumConfigureErrorPhase) Valid() bool { + switch e { + case ConfigurePhase: + return true + case NavigatePhase: + return true + default: + return false + } +} + // Defines values for ClickMouseRequestButton. const ( ClickMouseRequestButtonBack ClickMouseRequestButton = "back" @@ -1817,6 +1835,17 @@ type BrowserTelemetryConfig struct { Browser *BrowserTelemetryCategoriesConfig `json:"browser,omitempty"` } +// ChromiumConfigureError Failure from batched chromium configure — includes which phase failed. +type ChromiumConfigureError struct { + Message string `json:"message"` + + // Phase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + Phase ChromiumConfigureErrorPhase `json:"phase"` +} + +// ChromiumConfigureErrorPhase configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. +type ChromiumConfigureErrorPhase string + // ClickMouseRequest defines model for ClickMouseRequest. type ClickMouseRequest struct { // Button Mouse button to interact with @@ -2465,6 +2494,33 @@ type InternalError = Error // NotFoundError defines model for NotFoundError. type NotFoundError = Error +// ChromiumConfigureMultipartBody defines parameters for ChromiumConfigure. +type ChromiumConfigureMultipartBody struct { + // ChromePolicies UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. + ChromePolicies *string `json:"chrome_policies,omitempty"` + + // ChromiumFlags UTF-8 JSON object `{"flags":["--kiosk"]}` — same semantics as PATCH /chromium/flags. + ChromiumFlags *string `json:"chromium_flags,omitempty"` + + // Display UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + Display *string `json:"display,omitempty"` + + // Extensions Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). + Extensions *[]struct { + Name string `json:"name"` + ZipFile openapi_types.File `json:"zip_file"` + } `json:"extensions,omitempty"` + + // ProfileArchive tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + ProfileArchive *openapi_types.File `json:"profile_archive,omitempty"` + + // StartUrl Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + StartUrl *string `json:"start_url,omitempty"` + + // StripComponents Leading path components to strip when extracting profile_archive (non-negative integer as text). + StripComponents *string `json:"strip_components,omitempty"` +} + // PatchChromiumFlagsJSONBody defines parameters for PatchChromiumFlags. type PatchChromiumFlagsJSONBody struct { // Flags Chromium flags to merge (e.g., ["--kiosk", "--disable-gpu"]) @@ -2590,6 +2646,9 @@ type StreamTelemetryEventsParams struct { LastEventID *string `json:"Last-Event-ID,omitempty"` } +// ChromiumConfigureMultipartRequestBody defines body for ChromiumConfigure for multipart/form-data ContentType. +type ChromiumConfigureMultipartRequestBody ChromiumConfigureMultipartBody + // PatchChromiumFlagsJSONRequestBody defines body for PatchChromiumFlags for application/json ContentType. type PatchChromiumFlagsJSONRequestBody PatchChromiumFlagsJSONBody @@ -3457,6 +3516,9 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // ChromiumConfigureWithBody request with any body + ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // PatchChromiumFlagsWithBody request with any body PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -3682,6 +3744,18 @@ type ClientInterface interface { StreamTelemetryEvents(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) ChromiumConfigureWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewChromiumConfigureRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPatchChromiumFlagsRequestWithBody(c.Server, contentType, body) if err != nil { @@ -4702,6 +4776,35 @@ func (c *Client) StreamTelemetryEvents(ctx context.Context, params *StreamTeleme return c.Client.Do(req) } +// NewChromiumConfigureRequestWithBody generates requests for ChromiumConfigure with any type of body +func NewChromiumConfigureRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/chromium/configure") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewPatchChromiumFlagsRequest calls the generic PatchChromiumFlags builder with application/json body func NewPatchChromiumFlagsRequest(server string, body PatchChromiumFlagsJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -6908,6 +7011,9 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // ChromiumConfigureWithBodyWithResponse request with any body + ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) + // PatchChromiumFlagsWithBodyWithResponse request with any body PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) @@ -7133,6 +7239,31 @@ type ClientWithResponsesInterface interface { StreamTelemetryEventsWithResponse(ctx context.Context, params *StreamTelemetryEventsParams, reqEditors ...RequestEditorFn) (*StreamTelemetryEventsResponse, error) } +type ChromiumConfigureResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *ChromiumConfigureError +} + +// Status returns HTTPResponse.Status +func (r ChromiumConfigureResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ChromiumConfigureResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PatchChromiumFlagsResponse struct { Body []byte HTTPResponse *http.Response @@ -8413,6 +8544,15 @@ func (r StreamTelemetryEventsResponse) StatusCode() int { return 0 } +// ChromiumConfigureWithBodyWithResponse request with arbitrary body returning *ChromiumConfigureResponse +func (c *ClientWithResponses) ChromiumConfigureWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ChromiumConfigureResponse, error) { + rsp, err := c.ChromiumConfigureWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseChromiumConfigureResponse(rsp) +} + // PatchChromiumFlagsWithBodyWithResponse request with arbitrary body returning *PatchChromiumFlagsResponse func (c *ClientWithResponses) PatchChromiumFlagsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchChromiumFlagsResponse, error) { rsp, err := c.PatchChromiumFlagsWithBody(ctx, contentType, body, reqEditors...) @@ -9147,6 +9287,53 @@ func (c *ClientWithResponses) StreamTelemetryEventsWithResponse(ctx context.Cont return ParseStreamTelemetryEventsResponse(rsp) } +// ParseChromiumConfigureResponse parses an HTTP response from a ChromiumConfigureWithResponse call +func ParseChromiumConfigureResponse(rsp *http.Response) (*ChromiumConfigureResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ChromiumConfigureResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ChromiumConfigureError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -11173,6 +11360,9 @@ func ParseStreamTelemetryEventsResponse(rsp *http.Response) (*StreamTelemetryEve // ServerInterface represents all server handlers. type ServerInterface interface { + // Apply batched Chromium filesystem and launch configuration plus optional navigation + // (POST /chromium/configure) + ChromiumConfigure(w http.ResponseWriter, r *http.Request) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) @@ -11341,6 +11531,12 @@ type ServerInterface interface { type Unimplemented struct{} +// Apply batched Chromium filesystem and launch configuration plus optional navigation +// (POST /chromium/configure) +func (_ Unimplemented) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Update Chromium launch flags and restart // (PATCH /chromium/flags) func (_ Unimplemented) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -11674,6 +11870,20 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// ChromiumConfigure operation middleware +func (siw *ServerInterfaceWrapper) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ChromiumConfigure(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PatchChromiumFlags operation middleware func (siw *ServerInterfaceWrapper) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { @@ -12839,6 +13049,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl ErrorHandlerFunc: options.ErrorHandlerFunc, } + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/chromium/configure", wrapper.ChromiumConfigure) + }) r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/chromium/flags", wrapper.PatchChromiumFlags) }) @@ -13013,6 +13226,50 @@ type InternalErrorJSONResponse Error type NotFoundErrorJSONResponse Error +type ChromiumConfigureRequestObject struct { + Body *multipart.Reader +} + +type ChromiumConfigureResponseObject interface { + VisitChromiumConfigureResponse(w http.ResponseWriter) error +} + +type ChromiumConfigure200JSONResponse OkResponse + +func (response ChromiumConfigure200JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ChromiumConfigure400JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response ChromiumConfigure409JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type ChromiumConfigure500JSONResponse ChromiumConfigureError + +func (response ChromiumConfigure500JSONResponse) VisitChromiumConfigureResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type PatchChromiumFlagsRequestObject struct { Body *PatchChromiumFlagsJSONRequestBody } @@ -15256,6 +15513,9 @@ func (response StreamTelemetryEvents200TexteventStreamResponse) VisitStreamTelem // StrictServerInterface represents all server handlers. type StrictServerInterface interface { + // Apply batched Chromium filesystem and launch configuration plus optional navigation + // (POST /chromium/configure) + ChromiumConfigure(ctx context.Context, request ChromiumConfigureRequestObject) (ChromiumConfigureResponseObject, error) // Update Chromium launch flags and restart // (PATCH /chromium/flags) PatchChromiumFlags(ctx context.Context, request PatchChromiumFlagsRequestObject) (PatchChromiumFlagsResponseObject, error) @@ -15449,6 +15709,37 @@ type strictHandler struct { options StrictHTTPServerOptions } +// ChromiumConfigure operation middleware +func (sh *strictHandler) ChromiumConfigure(w http.ResponseWriter, r *http.Request) { + var request ChromiumConfigureRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ChromiumConfigure(ctx, request.(ChromiumConfigureRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ChromiumConfigure") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ChromiumConfigureResponseObject); ok { + if err := validResponse.VisitChromiumConfigureResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // PatchChromiumFlags operation middleware func (sh *strictHandler) PatchChromiumFlags(w http.ResponseWriter, r *http.Request) { var request PatchChromiumFlagsRequestObject @@ -17042,283 +17333,296 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "/wEAAP//c4aJWK9pAQA=", + "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", + "AsYNO8cHwcG5YxLPlq71/x8AAP//rDom6iV1AQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 1c913ed1..ab2d75b7 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1169,6 +1169,79 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + + /chromium/configure: + post: + summary: Apply batched Chromium filesystem and launch configuration plus optional navigation + description: | + Optional multipart parts apply configuration while Chromium stays stopped once (policy, + flags, extensions, profile archive, optional display sizing), then Chromium is started exactly + once and DevTools readiness is awaited. Optional `start_url` applies a Page.navigate via CDP + after readiness. Omit any part you do not need. At least one actionable part must be present. + Prefer this over separate `/chromium/*` and `/display` calls when multiple restart-triggering + steps apply in one session configure. + operationId: chromiumConfigure + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + display: + description: >- + UTF-8 JSON object matching `#/components/schemas/PatchDisplayRequest` (width/height/etc.). + type: string + chromium_flags: + description: >- + UTF-8 JSON object `{"flags":["--kiosk"]}` — same semantics as PATCH /chromium/flags. + type: string + chrome_policies: + description: UTF-8 JSON policy override map — same semantics as PATCH /chromium/policies. + type: string + profile_archive: + description: >- + tar.zst of `/home/kernel/user-data` (V2 profiles). Stripped paths use strip_components optional part. + type: string + format: binary + strip_components: + description: Leading path components to strip when extracting profile_archive (non-negative integer as text). + type: string + start_url: + description: >- + Bare https? URL text, OR UTF-8 JSON `{"url":"...", "wait_until":"load"|"domcontentloaded"}`. + type: string + extensions: + type: array + description: Extension zips paired with consecutive extensions.name fields (same as upload-extensions-and-restart). + items: + type: object + properties: + zip_file: + type: string + format: binary + name: + type: string + pattern: "^[A-Za-z0-9._-]{1,255}$" + required: [zip_file, name] + responses: + "200": + description: Configuration applied; optional navigate completed successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/OkResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + description: Configure or navigate failure with structured phase + content: + application/json: + schema: + $ref: "#/components/schemas/ChromiumConfigureError" + /playwright/execute: post: summary: Execute Playwright/TypeScript code against the browser @@ -2491,6 +2564,20 @@ components: properties: message: type: string + ChromiumConfigureError: + type: object + description: Failure from batched chromium configure — includes which phase failed. + required: [phase, message] + additionalProperties: false + properties: + phase: + type: string + enum: + - configure_phase + - navigate_phase + description: configure_phase maps to restart/filesystem/policy/extension/profile/display work; navigate_phase maps to Page.navigate after readiness. + message: + type: string RecorderInfo: type: object required: [id, isRecording]