From 7a4f0319a16d4695b5ec07ce0bdb464da116779e Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 19 Dec 2025 19:42:39 +0000 Subject: [PATCH 01/11] feat: update policy with requried values --- server/lib/policy/policy.go | 42 +++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index 91861383..81a1eb24 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "slices" "sync" ) @@ -17,13 +18,14 @@ type Policy struct { AutofillCreditCardEnabled bool `json:"AutofillCreditCardEnabled"` TranslateEnabled bool `json:"TranslateEnabled"` DefaultNotificationsSetting int `json:"DefaultNotificationsSetting"` + ExtensionInstallForcelist []string `json:"ExtensionInstallForcelist,omitempty"` ExtensionSettings map[string]ExtensionSetting `json:"ExtensionSettings"` } // ExtensionSetting represents settings for a specific extension type ExtensionSetting struct { InstallationMode string `json:"installation_mode,omitempty"` - Path string `json:"path,omitempty"` + UpdateUrl string `json:"update_url,omitempty"` AllowedTypes []string `json:"allowed_types,omitempty"` InstallSources []string `json:"install_sources,omitempty"` RuntimeBlockedHosts []string `json:"runtime_blocked_hosts,omitempty"` @@ -42,6 +44,7 @@ func (p *Policy) readPolicyUnlocked() (*Policy, error) { AutofillCreditCardEnabled: false, TranslateEnabled: false, DefaultNotificationsSetting: 2, + ExtensionInstallForcelist: []string{}, ExtensionSettings: make(map[string]ExtensionSetting), }, nil } @@ -58,6 +61,11 @@ func (p *Policy) readPolicyUnlocked() (*Policy, error) { policy.ExtensionSettings = make(map[string]ExtensionSetting) } + // Initialize ExtensionInstallForcelist if it's nil + if policy.ExtensionInstallForcelist == nil { + policy.ExtensionInstallForcelist = []string{} + } + return &policy, nil } @@ -93,7 +101,8 @@ func (p *Policy) WritePolicy(policy *Policy) error { } // AddExtension adds or updates an extension in the policy -// extensionID should be a stable identifier (can be derived from extension path) +// extensionID should be the extension name for ExtensionSettings +// extensionPath is the full path to the unpacked extension directory func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterprisePolicy bool) error { // Lock for the entire read-modify-write cycle to prevent race conditions p.mu.Lock() @@ -114,21 +123,36 @@ func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterpr // Add the specific extension setting := ExtensionSetting{ - Path: extensionPath, + UpdateUrl: "http://127.0.0.1:10001/update.xml", } // If the extension requires enterprise policy (like webRequestBlocking), - // set it as force_installed https://github.com/cloudflare/web-bot-auth/blob/main/examples/browser-extension/policy/policy.json.templ + // we need special handling for unpacked extensions loaded via --load-extension + // https://github.com/cloudflare/web-bot-auth/blob/main/examples/browser-extension/policy/policy.json.templ if requiresEnterprisePolicy { + // For unpacked extensions with webRequestBlocking: + // Chrome requires the extension to be in ExtensionInstallForcelist + // Format: "extension_id;update_url" per https://chromeenterprise.google/intl/en_ca/policies/#ExtensionInstallForcelist setting.InstallationMode = "force_installed" - // Allow all hosts for webRequest APIs - setting.RuntimeAllowedHosts = []string{"*://*/*"} + + // Add to ExtensionInstallForcelist using the format: extension_id;update_url + forcelistEntry := fmt.Sprintf("%s;%s", extensionID, setting.UpdateUrl) + + // Check if already in forcelist + found := slices.Contains(policy.ExtensionInstallForcelist, forcelistEntry) + if !found { + if policy.ExtensionInstallForcelist == nil { + policy.ExtensionInstallForcelist = []string{} + } + policy.ExtensionInstallForcelist = append(policy.ExtensionInstallForcelist, forcelistEntry) + } + + policy.ExtensionSettings[extensionID] = setting } else { - setting.InstallationMode = "normal_installed" + // For normal extensions, use the custom ID + policy.ExtensionSettings[extensionID] = setting } - policy.ExtensionSettings[extensionID] = setting - return p.writePolicyUnlocked(policy) } From 905d8d95e278fa847c4d72fc3d59625d29a093e2 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 19 Dec 2025 19:43:19 +0000 Subject: [PATCH 02/11] feat: update api to host update.xml and retrive .crx --- images/chromium-headful/run-docker.sh | 2 +- server/cmd/api/api/chromium.go | 43 +++++++++++++++----- server/cmd/api/main.go | 58 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/images/chromium-headful/run-docker.sh b/images/chromium-headful/run-docker.sh index c3a2972b..f99c976f 100755 --- a/images/chromium-headful/run-docker.sh +++ b/images/chromium-headful/run-docker.sh @@ -63,7 +63,7 @@ RUN_ARGS=( -e WIDTH=1920 -e TZ=${TZ:-'America/Los_Angeles'} -e RUN_AS_ROOT="$RUN_AS_ROOT" - --mount type=bind,src="$FLAGS_FILE",dst=/chromium/flags,ro + --mount type=bind,src="$FLAGS_FILE",dst=/chromium/flags ) if [[ -n "${PLAYWRIGHT_ENGINE:-}" ]]; then diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 20bab42f..78c1f94c 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -157,10 +157,34 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap log.Error("failed to chown extension dir", "error", err) return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to chown extension dir"}}, nil } + + // Check if the zip contains update.xml and .crx files (for policy-installed extensions) + // If they exist, they'll be extracted; if not, they need to be generated separately + updateXMLPath := filepath.Join(dest, "update.xml") + crxPath := filepath.Join(dest, p.name+".crx") + hasUpdateXML := false + hasCRX := false + + if _, err := os.Stat(updateXMLPath); err == nil { + hasUpdateXML = true + log.Info("found update.xml in extension zip", "name", p.name) + } + if _, err := os.Stat(crxPath); err == nil { + hasCRX = true + log.Info("found .crx file in extension zip", "name", p.name) + } + + if !hasUpdateXML || !hasCRX { + log.Info("extension zip missing update.xml or .crx - these files should be included for policy-installed extensions", "name", p.name, "hasUpdateXML", hasUpdateXML, "hasCRX", hasCRX) + } + 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 { extensionPath := filepath.Join(extBase, p.name) extensionID := s.policy.GenerateExtensionID(p.name) @@ -175,6 +199,9 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap if requiresEntPolicy { log.Info("extension requires enterprise policy", "name", p.name) + } else { + // Only add --load-extension flags for non-policy extensions + pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) } // Add to enterprise policy @@ -191,15 +218,13 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } // Build flags overlay file in /chromium/flags, merging with existing flags - var paths []string - for _, p := range items { - paths = append(paths, filepath.Join(extBase, p.name)) - } - - // Create new flags for the uploaded extensions - newTokens := []string{ - fmt.Sprintf("--disable-extensions-except=%s", strings.Join(paths, ",")), - fmt.Sprintf("--load-extension=%s", strings.Join(paths, ",")), + // Only add --load-extension flags for extensions that don't use policy installation + var newTokens []string + if len(pathsNeedingFlags) > 0 { + newTokens = []string{ + fmt.Sprintf("--disable-extensions-except=%s", strings.Join(pathsNeedingFlags, ",")), + fmt.Sprintf("--load-extension=%s", strings.Join(pathsNeedingFlags, ",")), + } } // Merge and write flags diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 6acdcad9..bb00f053 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -124,6 +124,64 @@ func main() { apiService.HandleProcessAttach(w, r, id) }) + // Serve extension files for Chrome policy-installed extensions + // This allows Chrome to download .crx and update.xml files via HTTP + extensionsDir := "/home/kernel/extensions" + r.Get("/extensions/*", func(w http.ResponseWriter, r *http.Request) { + // Serve files from /home/kernel/extensions/ + fs := http.StripPrefix("/extensions/", http.FileServer(http.Dir(extensionsDir))) + fs.ServeHTTP(w, r) + }) + + // Serve update.xml at root for Chrome enterprise policy + // This serves the first update.xml found in any extension directory + r.Get("/update.xml", func(w http.ResponseWriter, r *http.Request) { + // Try to find update.xml in the first extension directory + entries, err := os.ReadDir(extensionsDir) + if err != nil { + http.Error(w, "extensions directory not found", http.StatusNotFound) + return + } + + for _, entry := range entries { + if entry.IsDir() { + updateXMLPath := fmt.Sprintf("%s/%s/update.xml", extensionsDir, entry.Name()) + if _, err := os.Stat(updateXMLPath); err == nil { + http.ServeFile(w, r, updateXMLPath) + return + } + } + } + + http.Error(w, "update.xml not found", http.StatusNotFound) + }) + + // Serve CRX files at root for Chrome enterprise policy + // This allows simple codebase URLs like http://host:port/extension-name.crx + r.Get("/{filename}.crx", func(w http.ResponseWriter, r *http.Request) { + // Extract the filename from the URL path + filename := chi.URLParam(r, "filename") + ".crx" + + // Search for the CRX file in all extension directories + entries, err := os.ReadDir(extensionsDir) + if err != nil { + http.Error(w, "extensions directory not found", http.StatusNotFound) + return + } + + for _, entry := range entries { + if entry.IsDir() { + crxPath := fmt.Sprintf("%s/%s/%s", extensionsDir, entry.Name(), filename) + if _, err := os.Stat(crxPath); err == nil { + http.ServeFile(w, r, crxPath) + return + } + } + } + + http.Error(w, "crx file not found", http.StatusNotFound) + }) + srv := &http.Server{ Addr: fmt.Sprintf(":%d", config.Port), Handler: r, From f9e1cee577cd41362564ed190a5cb9deb3b5a943 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Wed, 7 Jan 2026 15:45:15 +0000 Subject: [PATCH 03/11] update location of update.xml --- server/cmd/api/api/chromium.go | 52 +++++++++++++++++++++------------- server/lib/policy/policy.go | 3 +- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 78c1f94c..426e03c1 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -158,26 +158,6 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap return oapi.UploadExtensionsAndRestart500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to chown extension dir"}}, nil } - // Check if the zip contains update.xml and .crx files (for policy-installed extensions) - // If they exist, they'll be extracted; if not, they need to be generated separately - updateXMLPath := filepath.Join(dest, "update.xml") - crxPath := filepath.Join(dest, p.name+".crx") - hasUpdateXML := false - hasCRX := false - - if _, err := os.Stat(updateXMLPath); err == nil { - hasUpdateXML = true - log.Info("found update.xml in extension zip", "name", p.name) - } - if _, err := os.Stat(crxPath); err == nil { - hasCRX = true - log.Info("found .crx file in extension zip", "name", p.name) - } - - if !hasUpdateXML || !hasCRX { - log.Info("extension zip missing update.xml or .crx - these files should be included for policy-installed extensions", "name", p.name, "hasUpdateXML", hasUpdateXML, "hasCRX", hasCRX) - } - log.Info("installed extension", "name", p.name) } @@ -199,6 +179,38 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap if requiresEntPolicy { log.Info("extension requires enterprise policy", "name", p.name) + + // Validate that update.xml and .crx files are present for policy-installed extensions + // These files are required for ExtensionInstallForcelist to work + updateXMLPath := filepath.Join(extensionPath, "update.xml") + hasUpdateXML := false + hasCRX := false + + if _, err := os.Stat(updateXMLPath); err == nil { + hasUpdateXML = true + log.Info("found update.xml in extension zip", "name", p.name) + } + + // Look for any .crx file in the directory + entries, err := os.ReadDir(extensionPath) + if err == nil { + for _, entry := range entries { + if !entry.IsDir() && filepath.Ext(entry.Name()) == ".crx" { + hasCRX = true + log.Info("found .crx file in extension zip", "name", p.name, "crx_file", entry.Name()) + break + } + } + } + + // Fail if policy extension is missing required files + if !hasUpdateXML || !hasCRX { + return oapi.UploadExtensionsAndRestart400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: fmt.Sprintf("extension %s requires enterprise policy (ExtensionInstallForcelist) but is missing required files: update.xml (present: %v), .crx file (present: %v). These files are required for Chrome to install the extension.", p.name, hasUpdateXML, hasCRX), + }, + }, nil + } } else { // Only add --load-extension flags for non-policy extensions pathsNeedingFlags = append(pathsNeedingFlags, extensionPath) diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index 81a1eb24..50ed1997 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -122,8 +122,9 @@ func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterpr } // Add the specific extension + // Use extension-specific update URL to support multiple policy-installed extensions setting := ExtensionSetting{ - UpdateUrl: "http://127.0.0.1:10001/update.xml", + UpdateUrl: fmt.Sprintf("http://127.0.0.1:10001/extensions/%s/update.xml", extensionID), } // If the extension requires enterprise policy (like webRequestBlocking), From fe4f952726175e7fe6218576af845f640246b292 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 9 Jan 2026 19:09:51 +0000 Subject: [PATCH 04/11] review: update the extension id to name --- server/cmd/api/api/chromium.go | 34 +++++++++++++------- server/lib/policy/policy.go | 58 ++++++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 426e03c1..9f83d197 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -15,6 +15,7 @@ import ( "github.com/onkernel/kernel-images/server/lib/chromiumflags" "github.com/onkernel/kernel-images/server/lib/logger" oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/policy" "github.com/onkernel/kernel-images/server/lib/ziputil" ) @@ -167,28 +168,38 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap for _, p := range items { extensionPath := filepath.Join(extBase, p.name) - extensionID := s.policy.GenerateExtensionID(p.name) + extensionName := p.name manifestPath := filepath.Join(extensionPath, "manifest.json") + updateXMLPath := filepath.Join(extensionPath, "update.xml") + + // Try to extract Chrome extension ID from update.xml + // If update.xml exists and contains an appid, use it; otherwise fall back to extension name + chromeExtensionID := extensionName + if extractedID, err := policy.ExtractExtensionIDFromUpdateXML(updateXMLPath); err == nil { + chromeExtensionID = extractedID + log.Info("extracted Chrome extension ID from update.xml", "name", extensionName, "chromeExtensionID", chromeExtensionID) + } else { + log.Info("no Chrome extension ID in update.xml, using name as ID", "name", extensionName, "error", err) + } // 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", p.name) + log.Warn("failed to read manifest for policy check", "error", err, "extension", extensionName) // Continue with requiresEntPolicy = false } if requiresEntPolicy { - log.Info("extension requires enterprise policy", "name", p.name) + 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 - updateXMLPath := filepath.Join(extensionPath, "update.xml") hasUpdateXML := false hasCRX := false if _, err := os.Stat(updateXMLPath); err == nil { hasUpdateXML = true - log.Info("found update.xml in extension zip", "name", p.name) + log.Info("found update.xml in extension zip", "name", extensionName) } // Look for any .crx file in the directory @@ -197,7 +208,7 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap for _, entry := range entries { if !entry.IsDir() && filepath.Ext(entry.Name()) == ".crx" { hasCRX = true - log.Info("found .crx file in extension zip", "name", p.name, "crx_file", entry.Name()) + log.Info("found .crx file in extension zip", "name", extensionName, "crx_file", entry.Name()) break } } @@ -207,7 +218,7 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap if !hasUpdateXML || !hasCRX { return oapi.UploadExtensionsAndRestart400JSONResponse{ BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ - Message: fmt.Sprintf("extension %s requires enterprise policy (ExtensionInstallForcelist) but is missing required files: update.xml (present: %v), .crx file (present: %v). These files are required for Chrome to install the extension.", p.name, hasUpdateXML, hasCRX), + Message: fmt.Sprintf("extension %s requires enterprise policy (ExtensionInstallForcelist) but is missing required files: update.xml (present: %v), .crx file (present: %v). These files are required for Chrome to install the extension.", extensionName, hasUpdateXML, hasCRX), }, }, nil } @@ -217,16 +228,17 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap } // Add to enterprise policy - if err := s.policy.AddExtension(extensionID, extensionPath, requiresEntPolicy); err != nil { - log.Error("failed to update enterprise policy", "error", err, "extension", p.name) + // 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", p.name, err), + Message: fmt.Sprintf("failed to update enterprise policy for %s: %v", extensionName, err), }, }, nil } - log.Info("updated enterprise policy", "extension", p.name, "id", extensionID, "requiresEnterprisePolicy", requiresEntPolicy) + log.Info("updated enterprise policy", "extension", extensionName, "chromeExtensionID", chromeExtensionID, "requiresEnterprisePolicy", requiresEntPolicy) } // Build flags overlay file in /chromium/flags, merging with existing flags diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index 50ed1997..f7082d20 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -2,6 +2,7 @@ package policy import ( "encoding/json" + "encoding/xml" "fmt" "os" "slices" @@ -101,9 +102,10 @@ func (p *Policy) WritePolicy(policy *Policy) error { } // AddExtension adds or updates an extension in the policy -// extensionID should be the extension name for ExtensionSettings +// extensionName is the user-provided name used for the directory and URL paths +// chromeExtensionID is the actual Chrome extension ID (from update.xml appid) used in policy entries // extensionPath is the full path to the unpacked extension directory -func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterprisePolicy bool) error { +func (p *Policy) AddExtension(extensionName, chromeExtensionID, extensionPath string, requiresEnterprisePolicy bool) error { // Lock for the entire read-modify-write cycle to prevent race conditions p.mu.Lock() defer p.mu.Unlock() @@ -122,9 +124,10 @@ func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterpr } // Add the specific extension - // Use extension-specific update URL to support multiple policy-installed extensions + // Use extension name for the URL path (where files are served) + // Use Chrome extension ID for the policy key (what Chrome expects) setting := ExtensionSetting{ - UpdateUrl: fmt.Sprintf("http://127.0.0.1:10001/extensions/%s/update.xml", extensionID), + UpdateUrl: fmt.Sprintf("http://127.0.0.1:10001/extensions/%s/update.xml", extensionName), } // If the extension requires enterprise policy (like webRequestBlocking), @@ -136,8 +139,8 @@ func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterpr // Format: "extension_id;update_url" per https://chromeenterprise.google/intl/en_ca/policies/#ExtensionInstallForcelist setting.InstallationMode = "force_installed" - // Add to ExtensionInstallForcelist using the format: extension_id;update_url - forcelistEntry := fmt.Sprintf("%s;%s", extensionID, setting.UpdateUrl) + // Add to ExtensionInstallForcelist using the Chrome extension ID and update URL + forcelistEntry := fmt.Sprintf("%s;%s", chromeExtensionID, setting.UpdateUrl) // Check if already in forcelist found := slices.Contains(policy.ExtensionInstallForcelist, forcelistEntry) @@ -148,10 +151,11 @@ func (p *Policy) AddExtension(extensionID, extensionPath string, requiresEnterpr policy.ExtensionInstallForcelist = append(policy.ExtensionInstallForcelist, forcelistEntry) } - policy.ExtensionSettings[extensionID] = setting + // Use Chrome extension ID as the key in ExtensionSettings + policy.ExtensionSettings[chromeExtensionID] = setting } else { - // For normal extensions, use the custom ID - policy.ExtensionSettings[extensionID] = setting + // For normal extensions, use the extension name as the key + policy.ExtensionSettings[extensionName] = setting } return p.writePolicyUnlocked(policy) @@ -193,3 +197,39 @@ func (p *Policy) RequiresEnterprisePolicy(manifestPath string) (bool, error) { return false, nil } + +// updateManifest represents the Chrome extension update manifest XML structure +type updateManifest struct { + XMLName xml.Name `xml:"gupdate"` + Apps []appNode `xml:"app"` +} + +type appNode struct { + AppID string `xml:"appid,attr"` +} + +// ExtractExtensionIDFromUpdateXML reads update.xml and extracts the appid attribute +// from the element. Returns the appid or an error if the file doesn't exist +// or the appid cannot be found. +func ExtractExtensionIDFromUpdateXML(updateXMLPath string) (string, error) { + data, err := os.ReadFile(updateXMLPath) + if err != nil { + return "", fmt.Errorf("failed to read update.xml: %w", err) + } + + var manifest updateManifest + if err := xml.Unmarshal(data, &manifest); err != nil { + return "", fmt.Errorf("failed to parse update.xml: %w", err) + } + + if len(manifest.Apps) == 0 { + return "", fmt.Errorf("no element found in update.xml") + } + + appID := manifest.Apps[0].AppID + if appID == "" { + return "", fmt.Errorf("appid attribute is empty in update.xml") + } + + return appID, nil +} From e52fc3f90fea0228111d18d71631613542a30ef3 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 9 Jan 2026 19:37:28 +0000 Subject: [PATCH 05/11] review --- server/cmd/api/api/chromium.go | 25 +++++++++++++++++-------- server/lib/policy/policy.go | 26 +++++++++++++++++++------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 9f83d197..09b96c66 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -172,23 +172,24 @@ 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 - // If update.xml exists and contains an appid, use it; otherwise fall back to extension name chromeExtensionID := extensionName + extractionErr := error(nil) if extractedID, err := policy.ExtractExtensionIDFromUpdateXML(updateXMLPath); err == nil { chromeExtensionID = extractedID log.Info("extracted Chrome extension ID from update.xml", "name", extensionName, "chromeExtensionID", chromeExtensionID) } else { + extractionErr = err log.Info("no Chrome extension ID in update.xml, using name as ID", "name", extensionName, "error", err) } - // 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 - } - if requiresEntPolicy { log.Info("extension requires enterprise policy", "name", extensionName) @@ -198,6 +199,14 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap 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 + } hasUpdateXML = true log.Info("found update.xml in extension zip", "name", extensionName) } diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index f7082d20..497f02f9 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "fmt" "os" + "regexp" "slices" "sync" ) @@ -142,15 +143,20 @@ func (p *Policy) AddExtension(extensionName, chromeExtensionID, extensionPath st // Add to ExtensionInstallForcelist using the Chrome extension ID and update URL forcelistEntry := fmt.Sprintf("%s;%s", chromeExtensionID, setting.UpdateUrl) - // Check if already in forcelist - found := slices.Contains(policy.ExtensionInstallForcelist, forcelistEntry) - if !found { - if policy.ExtensionInstallForcelist == nil { - policy.ExtensionInstallForcelist = []string{} - } - policy.ExtensionInstallForcelist = append(policy.ExtensionInstallForcelist, forcelistEntry) + // Remove any existing entries with the same extension ID (different URLs) + if policy.ExtensionInstallForcelist == nil { + policy.ExtensionInstallForcelist = []string{} } + // Filter out entries that start with the same extension ID + extensionIDPrefix := chromeExtensionID + ";" + policy.ExtensionInstallForcelist = slices.DeleteFunc(policy.ExtensionInstallForcelist, func(entry string) bool { + return len(entry) >= len(extensionIDPrefix) && entry[:len(extensionIDPrefix)] == extensionIDPrefix + }) + + // Add the new entry + policy.ExtensionInstallForcelist = append(policy.ExtensionInstallForcelist, forcelistEntry) + // Use Chrome extension ID as the key in ExtensionSettings policy.ExtensionSettings[chromeExtensionID] = setting } else { @@ -231,5 +237,11 @@ func ExtractExtensionIDFromUpdateXML(updateXMLPath string) (string, error) { return "", fmt.Errorf("appid attribute is empty in update.xml") } + // Validate extension ID format: Chrome extension IDs are 32 lowercase hex characters + // This prevents injection attacks via semicolons or other special characters + if !regexp.MustCompile(`^[a-p]{32}$`).MatchString(appID) { + return "", fmt.Errorf("invalid Chrome extension ID format in update.xml: %s", appID) + } + return appID, nil } From 058a6aa52b107aa02c29416125f6a74c28abb8e5 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 9 Jan 2026 20:42:17 +0000 Subject: [PATCH 06/11] review --- server/e2e/e2e_chromium_test.go | 112 -------------------------------- server/lib/policy/policy.go | 1 + 2 files changed, 1 insertion(+), 112 deletions(-) diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go index 8bc813f2..c3762bb4 100644 --- a/server/e2e/e2e_chromium_test.go +++ b/server/e2e/e2e_chromium_test.go @@ -804,115 +804,3 @@ func listCDPTargets(ctx context.Context) ([]map[string]interface{}, error) { return targets, nil } - -func TestWebBotAuthInstallation(t *testing.T) { - image := headlessImage - name := containerName + "-web-bot-auth" - - logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo})) - baseCtx := logctx.AddToContext(context.Background(), logger) - - if _, err := exec.LookPath("docker"); err != nil { - require.NoError(t, err, "docker not available: %v", err) - } - - // Clean slate - _ = stopContainer(baseCtx, name) - - env := map[string]string{} - - // Start container - _, exitCh, err := runContainer(baseCtx, image, name, env) - require.NoError(t, err, "failed to start container: %v", err) - defer stopContainer(baseCtx, name) - - ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) - defer cancel() - - logger.Info("[setup]", "action", "waiting for API", "url", apiBaseURL+"/spec.yaml") - require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) - - // Build mock web-bot-auth extension zip in-memory - extDir := t.TempDir() - manifest := `{ - "manifest_version": 3, - "version": "1.0.0", - "name": "Web Bot Auth Mock", - "description": "Mock web-bot-auth extension for testing", - "permissions": [ - "webRequest", - "webRequestBlocking" - ], - "host_permissions": [ - "*://*/*" - ] -}` - err = os.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(manifest), 0600) - require.NoError(t, err, "write manifest: %v", err) - - extZip, err := zipDirToBytes(extDir) - require.NoError(t, err, "zip ext: %v", err) - - // Upload extension using the API - { - client, err := apiClient() - require.NoError(t, err) - var body bytes.Buffer - w := multipart.NewWriter(&body) - fw, err := w.CreateFormFile("extensions.zip_file", "web-bot-auth.zip") - require.NoError(t, err) - _, err = io.Copy(fw, bytes.NewReader(extZip)) - require.NoError(t, err) - err = w.WriteField("extensions.name", "web-bot-auth") - require.NoError(t, err) - err = w.Close() - require.NoError(t, err) - - logger.Info("[test]", "action", "uploading web-bot-auth extension") - start := time.Now() - rsp, err := client.UploadExtensionsAndRestartWithBodyWithResponse(ctx, w.FormDataContentType(), &body) - elapsed := time.Since(start) - require.NoError(t, err, "uploadExtensionsAndRestart request error: %v", err) - require.Equal(t, http.StatusCreated, rsp.StatusCode(), "unexpected status: %s body=%s", rsp.Status(), string(rsp.Body)) - logger.Info("[test]", "action", "extension uploaded", "elapsed", elapsed.String()) - } - - // Verify the policy.json file contains the correct web-bot-auth configuration - { - logger.Info("[test]", "action", "reading policy.json") - policyContent, err := execCombinedOutput(ctx, "cat", []string{"/etc/chromium/policies/managed/policy.json"}) - require.NoError(t, err, "failed to read policy.json: %v", err) - - logger.Info("[test]", "policy_content", policyContent) - - var policy map[string]interface{} - err = json.Unmarshal([]byte(policyContent), &policy) - require.NoError(t, err, "failed to parse policy.json: %v", err) - - // Check ExtensionSettings exists - extensionSettings, ok := policy["ExtensionSettings"].(map[string]interface{}) - require.True(t, ok, "ExtensionSettings not found in policy.json") - - // Check web-bot-auth entry exists - webBotAuth, ok := extensionSettings["web-bot-auth"].(map[string]interface{}) - require.True(t, ok, "web-bot-auth entry not found in ExtensionSettings") - - // Verify installation_mode is force_installed - installationMode, ok := webBotAuth["installation_mode"].(string) - require.True(t, ok, "installation_mode not found in web-bot-auth entry") - require.Equal(t, "force_installed", installationMode, "expected installation_mode to be force_installed") - - // Verify path - path, ok := webBotAuth["path"].(string) - require.True(t, ok, "path not found in web-bot-auth entry") - require.Equal(t, "/home/kernel/extensions/web-bot-auth", path, "expected path to be /home/kernel/extensions/web-bot-auth") - - // Verify runtime_allowed_hosts - runtimeAllowedHosts, ok := webBotAuth["runtime_allowed_hosts"].([]interface{}) - require.True(t, ok, "runtime_allowed_hosts not found in web-bot-auth entry") - require.Len(t, runtimeAllowedHosts, 1, "expected runtime_allowed_hosts to have 1 entry") - require.Equal(t, "*://*/*", runtimeAllowedHosts[0].(string), "expected runtime_allowed_hosts to contain *://*/*") - - logger.Info("[test]", "result", "web-bot-auth policy verified successfully") - } -} diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index 497f02f9..d81ee510 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -139,6 +139,7 @@ func (p *Policy) AddExtension(extensionName, chromeExtensionID, extensionPath st // Chrome requires the extension to be in ExtensionInstallForcelist // Format: "extension_id;update_url" per https://chromeenterprise.google/intl/en_ca/policies/#ExtensionInstallForcelist setting.InstallationMode = "force_installed" + setting.RuntimeAllowedHosts = []string{"*://*/*"} // Add to ExtensionInstallForcelist using the Chrome extension ID and update URL forcelistEntry := fmt.Sprintf("%s;%s", chromeExtensionID, setting.UpdateUrl) From 6196a1dbce4e6db83c38434e1ea2ffc1ffd34eae Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 12 Jan 2026 11:23:21 +0000 Subject: [PATCH 07/11] review: address feedback --- server/cmd/api/api/chromium.go | 2 +- server/cmd/api/main.go | 23 ----------------------- server/lib/policy/policy.go | 11 +++++++---- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 09b96c66..39a282f8 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -181,7 +181,7 @@ func (s *ApiService) UploadExtensionsAndRestart(ctx context.Context, request oap // Try to extract Chrome extension ID from update.xml chromeExtensionID := extensionName - extractionErr := error(nil) + var extractionErr error if extractedID, err := policy.ExtractExtensionIDFromUpdateXML(updateXMLPath); err == nil { chromeExtensionID = extractedID log.Info("extracted Chrome extension ID from update.xml", "name", extensionName, "chromeExtensionID", chromeExtensionID) diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index bb00f053..d745fc0c 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -133,29 +133,6 @@ func main() { fs.ServeHTTP(w, r) }) - // Serve update.xml at root for Chrome enterprise policy - // This serves the first update.xml found in any extension directory - r.Get("/update.xml", func(w http.ResponseWriter, r *http.Request) { - // Try to find update.xml in the first extension directory - entries, err := os.ReadDir(extensionsDir) - if err != nil { - http.Error(w, "extensions directory not found", http.StatusNotFound) - return - } - - for _, entry := range entries { - if entry.IsDir() { - updateXMLPath := fmt.Sprintf("%s/%s/update.xml", extensionsDir, entry.Name()) - if _, err := os.Stat(updateXMLPath); err == nil { - http.ServeFile(w, r, updateXMLPath) - return - } - } - } - - http.Error(w, "update.xml not found", http.StatusNotFound) - }) - // Serve CRX files at root for Chrome enterprise policy // This allows simple codebase URLs like http://host:port/extension-name.crx r.Get("/{filename}.crx", func(w http.ResponseWriter, r *http.Request) { diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index d81ee510..3eda5332 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -7,11 +7,15 @@ import ( "os" "regexp" "slices" + "strings" "sync" ) const PolicyPath = "/etc/chromium/policies/managed/policy.json" +// Chrome extension IDs are 32 lowercase a-p characters +var extensionIDRegex = regexp.MustCompile(`^[a-p]{32}$`) + // Policy represents the Chrome enterprise policy structure type Policy struct { mu sync.Mutex @@ -139,7 +143,6 @@ func (p *Policy) AddExtension(extensionName, chromeExtensionID, extensionPath st // Chrome requires the extension to be in ExtensionInstallForcelist // Format: "extension_id;update_url" per https://chromeenterprise.google/intl/en_ca/policies/#ExtensionInstallForcelist setting.InstallationMode = "force_installed" - setting.RuntimeAllowedHosts = []string{"*://*/*"} // Add to ExtensionInstallForcelist using the Chrome extension ID and update URL forcelistEntry := fmt.Sprintf("%s;%s", chromeExtensionID, setting.UpdateUrl) @@ -152,7 +155,7 @@ func (p *Policy) AddExtension(extensionName, chromeExtensionID, extensionPath st // Filter out entries that start with the same extension ID extensionIDPrefix := chromeExtensionID + ";" policy.ExtensionInstallForcelist = slices.DeleteFunc(policy.ExtensionInstallForcelist, func(entry string) bool { - return len(entry) >= len(extensionIDPrefix) && entry[:len(extensionIDPrefix)] == extensionIDPrefix + return strings.HasPrefix(entry, extensionIDPrefix) }) // Add the new entry @@ -238,9 +241,9 @@ func ExtractExtensionIDFromUpdateXML(updateXMLPath string) (string, error) { return "", fmt.Errorf("appid attribute is empty in update.xml") } - // Validate extension ID format: Chrome extension IDs are 32 lowercase hex characters + // Validate extension ID format: Chrome extension IDs are 32 lowercase a-p characters // This prevents injection attacks via semicolons or other special characters - if !regexp.MustCompile(`^[a-p]{32}$`).MatchString(appID) { + if !extensionIDRegex.MatchString(appID) { return "", fmt.Errorf("invalid Chrome extension ID format in update.xml: %s", appID) } From b2579e750b5fcfc1e4a88d3a863a35f0acf95318 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 12 Jan 2026 12:15:51 +0000 Subject: [PATCH 08/11] review: readd web-bot-auth e2e test --- server/e2e/e2e_chromium_test.go | 126 ++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go index c3762bb4..e4cb251b 100644 --- a/server/e2e/e2e_chromium_test.go +++ b/server/e2e/e2e_chromium_test.go @@ -804,3 +804,129 @@ func listCDPTargets(ctx context.Context) ([]map[string]interface{}, error) { return targets, nil } + +func TestWebBotAuthInstallation(t *testing.T) { + image := headlessImage + name := containerName + "-web-bot-auth" + + logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo})) + baseCtx := logctx.AddToContext(context.Background(), logger) + + if _, err := exec.LookPath("docker"); err != nil { + require.NoError(t, err, "docker not available: %v", err) + } + + // Clean slate + _ = stopContainer(baseCtx, name) + + env := map[string]string{} + + // Start container + _, exitCh, err := runContainer(baseCtx, image, name, env) + require.NoError(t, err, "failed to start container: %v", err) + defer stopContainer(baseCtx, name) + + ctx, cancel := context.WithTimeout(baseCtx, 3*time.Minute) + defer cancel() + + logger.Info("[setup]", "action", "waiting for API", "url", apiBaseURL+"/spec.yaml") + require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) + + // Build mock web-bot-auth extension zip in-memory + extDir := t.TempDir() + + // Create manifest with webRequest permissions to trigger enterprise policy requirement + manifest := map[string]interface{}{ + "manifest_version": 3, + "version": "1.0.0", + "name": "Web Bot Auth Mock", + "description": "Mock web-bot-auth extension for testing", + "permissions": []string{"webRequest", "webRequestBlocking"}, + "host_permissions": []string{""}, + } + manifestJSON, err := json.MarshalIndent(manifest, "", " ") + require.NoError(t, err, "marshal manifest: %v", err) + + err = os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestJSON, 0600) + require.NoError(t, err, "write manifest: %v", err) + + extZip, err := zipDirToBytes(extDir) + require.NoError(t, err, "zip ext: %v", err) + + // Upload extension using the API + { + client, err := apiClient() + require.NoError(t, err) + var body bytes.Buffer + w := multipart.NewWriter(&body) + fw, err := w.CreateFormFile("extensions.zip_file", "web-bot-auth.zip") + require.NoError(t, err) + _, err = io.Copy(fw, bytes.NewReader(extZip)) + require.NoError(t, err) + err = w.WriteField("extensions.name", "web-bot-auth") + require.NoError(t, err) + err = w.Close() + require.NoError(t, err) + + logger.Info("[test]", "action", "uploading web-bot-auth extension") + start := time.Now() + rsp, err := client.UploadExtensionsAndRestartWithBodyWithResponse(ctx, w.FormDataContentType(), &body) + elapsed := time.Since(start) + require.NoError(t, err, "uploadExtensionsAndRestart request error: %v", err) + require.Equal(t, http.StatusCreated, rsp.StatusCode(), "unexpected status: %s body=%s", rsp.Status(), string(rsp.Body)) + logger.Info("[test]", "action", "extension uploaded", "elapsed", elapsed.String()) + } + + // Verify the policy.json file contains the correct web-bot-auth configuration + { + logger.Info("[test]", "action", "reading policy.json") + policyContent, err := execCombinedOutput(ctx, "cat", []string{"/etc/chromium/policies/managed/policy.json"}) + require.NoError(t, err, "failed to read policy.json: %v", err) + + logger.Info("[test]", "policy_content", policyContent) + + var policy map[string]interface{} + err = json.Unmarshal([]byte(policyContent), &policy) + require.NoError(t, err, "failed to parse policy.json: %v", err) + + // Check ExtensionInstallForcelist exists + extensionInstallForcelist, ok := policy["ExtensionInstallForcelist"].([]interface{}) + require.True(t, ok, "ExtensionInstallForcelist not found in policy.json") + require.GreaterOrEqual(t, len(extensionInstallForcelist), 1, "ExtensionInstallForcelist should have at least 1 entry") + + // Find the web-bot-auth entry in the forcelist + var webBotAuthEntry string + for _, entry := range extensionInstallForcelist { + if entryStr, ok := entry.(string); ok && strings.Contains(entryStr, "web-bot-auth") { + webBotAuthEntry = entryStr + break + } + } + require.NotEmpty(t, webBotAuthEntry, "web-bot-auth entry not found in ExtensionInstallForcelist") + + // Verify the entry format: "extension-id;update_url" + parts := strings.Split(webBotAuthEntry, ";") + require.Len(t, parts, 2, "expected web-bot-auth entry to have format 'extension-id;update_url'") + + extensionID := parts[0] + updateURL := parts[1] + + logger.Info("[test]", "extension_id", extensionID, "update_url", updateURL) + logger.Info("[test]", "result", "web-bot-auth policy verified successfully") + } + + // Verify the extension directory exists + { + logger.Info("[test]", "action", "checking extension directory") + dirList, err := execCombinedOutput(ctx, "ls", []string{"-la", "/home/kernel/extensions/web-bot-auth/"}) + require.NoError(t, err, "failed to list extension directory: %v", err) + logger.Info("[test]", "extension_directory_contents", dirList) + + // Verify manifest.json exists (uploaded as part of the extension) + manifestContent, err := execCombinedOutput(ctx, "cat", []string{"/home/kernel/extensions/web-bot-auth/manifest.json"}) + require.NoError(t, err, "failed to read manifest.json: %v", err) + require.Contains(t, manifestContent, "Web Bot Auth Mock", "manifest.json should contain extension name") + + logger.Info("[test]", "result", "extension directory verified successfully") + } +} From a9882806d8761563b195cf8e52c05ff20188aaf4 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 12 Jan 2026 12:54:27 +0000 Subject: [PATCH 09/11] review: add update.xml to test --- server/e2e/e2e_chromium_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go index e4cb251b..a6261472 100644 --- a/server/e2e/e2e_chromium_test.go +++ b/server/e2e/e2e_chromium_test.go @@ -850,6 +850,30 @@ func TestWebBotAuthInstallation(t *testing.T) { err = os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestJSON, 0600) require.NoError(t, err, "write manifest: %v", err) + // Create update.xml required for enterprise policy + updateXML := map[string]interface{}{ + "gupdate": map[string]interface{}{ + "@xmlns": "http://www.google.com/update2/response", + "@protocol": "2.0", + "app": map[string]interface{}{ + "@appid": "aaaabbbbccccddddeeeeffffgggghhhh", + "updatecheck": map[string]interface{}{ + "@codebase": "http://localhost:10001/extensions/web-bot-auth/web-bot-auth.crx", + "@version": "1.0.0", + }, + }, + }, + } + + updateXMLJSON, err := json.MarshalIndent(updateXML, "", " ") + require.NoError(t, err, "marshal update.xml: %v", err) + err = os.WriteFile(filepath.Join(extDir, "update.xml"), updateXMLJSON, 0600) + require.NoError(t, err, "write update.xml: %v", err) + + // Create a minimal .crx file (just needs to exist for the test) + err = os.WriteFile(filepath.Join(extDir, "web-bot-auth.crx"), []byte("mock crx content"), 0600) + require.NoError(t, err, "write .crx: %v", err) + extZip, err := zipDirToBytes(extDir) require.NoError(t, err, "zip ext: %v", err) From 92993d058566f17df00151b890d0c687bda8f5b5 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 12 Jan 2026 13:17:42 +0000 Subject: [PATCH 10/11] review: update update.xml to test --- server/e2e/e2e_chromium_test.go | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/server/e2e/e2e_chromium_test.go b/server/e2e/e2e_chromium_test.go index a6261472..1c08f78c 100644 --- a/server/e2e/e2e_chromium_test.go +++ b/server/e2e/e2e_chromium_test.go @@ -851,23 +851,14 @@ func TestWebBotAuthInstallation(t *testing.T) { require.NoError(t, err, "write manifest: %v", err) // Create update.xml required for enterprise policy - updateXML := map[string]interface{}{ - "gupdate": map[string]interface{}{ - "@xmlns": "http://www.google.com/update2/response", - "@protocol": "2.0", - "app": map[string]interface{}{ - "@appid": "aaaabbbbccccddddeeeeffffgggghhhh", - "updatecheck": map[string]interface{}{ - "@codebase": "http://localhost:10001/extensions/web-bot-auth/web-bot-auth.crx", - "@version": "1.0.0", - }, - }, - }, - } - - updateXMLJSON, err := json.MarshalIndent(updateXML, "", " ") - require.NoError(t, err, "marshal update.xml: %v", err) - err = os.WriteFile(filepath.Join(extDir, "update.xml"), updateXMLJSON, 0600) + updateXMLContent := ` + + + + +` + + err = os.WriteFile(filepath.Join(extDir, "update.xml"), []byte(updateXMLContent), 0600) require.NoError(t, err, "write update.xml: %v", err) // Create a minimal .crx file (just needs to exist for the test) From 46a6e1e38d1272ece019aa7574522dd48bbea542 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 12 Jan 2026 13:53:32 +0000 Subject: [PATCH 11/11] review: remove old endpoint --- server/cmd/api/main.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index d745fc0c..50e377a0 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -133,32 +133,6 @@ func main() { fs.ServeHTTP(w, r) }) - // Serve CRX files at root for Chrome enterprise policy - // This allows simple codebase URLs like http://host:port/extension-name.crx - r.Get("/{filename}.crx", func(w http.ResponseWriter, r *http.Request) { - // Extract the filename from the URL path - filename := chi.URLParam(r, "filename") + ".crx" - - // Search for the CRX file in all extension directories - entries, err := os.ReadDir(extensionsDir) - if err != nil { - http.Error(w, "extensions directory not found", http.StatusNotFound) - return - } - - for _, entry := range entries { - if entry.IsDir() { - crxPath := fmt.Sprintf("%s/%s/%s", extensionsDir, entry.Name(), filename) - if _, err := os.Stat(crxPath); err == nil { - http.ServeFile(w, r, crxPath) - return - } - } - } - - http.Error(w, "crx file not found", http.StatusNotFound) - }) - srv := &http.Server{ Addr: fmt.Sprintf(":%d", config.Port), Handler: r,