From 01a228bf8311a0ca7785196ea21378c5f146c002 Mon Sep 17 00:00:00 2001 From: rajuGT Date: Tue, 27 Oct 2026 18:44:34 +0530 Subject: [PATCH] Install private and trusted-tester extensions from the Chrome Web Store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed A Chrome Web Store install now retries the package download once with the Space's cookies when Google's update service answers 401 or 403. Without this, a private or trusted-tester item can't be installed, because the service serves it only to an account that can see it. The first request still omits cookies, so a public item is downloaded anonymously as before. This matches how Chromium's extension downloader recovers an authenticated CRX fetch (IterateFetchCredentialsAfterFailure). Cancelling the install review still stops the retry. The download's network traffic annotation and the engine README describe when cookies are sent. This touches engine inputs (CrestEngines/Chromium/Overlay), so a maintainer needs to publish a new chromium-engine release with the merge. ## Validation - Scripts/check-version.sh --fix-commit passed (0.8.114 to 0.8.115, release note private-web-store-extension-install). - The Chromium engine was not rebuilt with this change, and installing a private item has not been tried yet. ## AI usage - [x] AI tools helped produce this change. - [x] I ran the `crest-contribution` skill (`.agents/skills/crest-contribution/SKILL.md`), followed it, and reviewed every generated line. ## Boundaries - [x] Space and browsing-profile isolation are preserved. - [x] The macOS and mobile platform behavior remains intentionally native. - [x] User-visible or architecture-significant changes add an explicit entry to `Documentation/ReleaseNotes.json`. - [x] Every verified fix commit includes its patch bump and passes `Scripts/check-version.sh --fix-commit`. - [x] I have signed off every commit as described in `CONTRIBUTING.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: rajuGT --- Config/Version.xcconfig | 2 +- .../browser/ui/crest/crest_chrome_host.mm | 119 +++++++++++++----- CrestEngines/Chromium/README.md | 9 +- Documentation/ReleaseNotes.json | 4 + 4 files changed, 96 insertions(+), 38 deletions(-) diff --git a/Config/Version.xcconfig b/Config/Version.xcconfig index 8b47167a1..1d8d47fba 100644 --- a/Config/Version.xcconfig +++ b/Config/Version.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 0.8.114 +MARKETING_VERSION = 0.8.115 diff --git a/CrestEngines/Chromium/Overlay/chrome/browser/ui/crest/crest_chrome_host.mm b/CrestEngines/Chromium/Overlay/chrome/browser/ui/crest/crest_chrome_host.mm index 856f2c2ed..902557332 100644 --- a/CrestEngines/Chromium/Overlay/chrome/browser/ui/crest/crest_chrome_host.mm +++ b/CrestEngines/Chromium/Overlay/chrome/browser/ui/crest/crest_chrome_host.mm @@ -58,6 +58,7 @@ #include "extensions/browser/install/crx_install_error.h" #include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" #include "base/memory/weak_ptr.h" #include "base/strings/escape.h" #include "base/task/thread_pool.h" @@ -65,7 +66,9 @@ #include "components/update_client/update_query_params.h" #include "components/version_info/version_info.h" #include "content/public/browser/storage_partition.h" +#include "net/cookies/site_for_cookies.h" #include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" #include "net/traffic_annotation/network_traffic_annotation.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/shared_url_loader_factory.h" @@ -721,11 +724,15 @@ void DeliverExtensionCommand(Profile* profile, const extensions::Extension& exte extensions::EventRouter::Get(profile)->DispatchEventToExtension(extension.id(), std::move(event)); } -// One Chrome Web Store package download, made by the Space's own profile with -// the request Chromium's Web Store installer sends. The ungoogled baseline -// empties that installer and rewrites Google's hosts in its sources, so the -// update service is named here. Chromium's CrxInstaller judges the package; -// nothing here limits or inspects it. Deletes itself once it has reported. +// One Chrome Web Store package download, made by the Space's own profile. The +// ungoogled baseline empties Chromium's installer and rewrites Google's hosts +// in its sources, so the update service is named here. A public item needs no +// account, and that request omits cookies. A private or trusted-tester item is +// served only to an account that can see it: when the service answers 401 or +// 403, the same download is retried once with the Space's cookies, which is +// how Chromium's extension downloader recovers an authenticated CRX fetch. +// Chromium's CrxInstaller judges the package; nothing here limits or inspects +// it. Deletes itself once it has reported. class WebStoreDownload { public: WebStoreDownload(void (^progress)(double), void (^completion)(NSString*, NSString*)) @@ -742,41 +749,69 @@ void Cancel() { } void Start(Profile* profile, const std::string& extension_id) { - auto request = std::make_unique(); - request->url = GURL("https://clients2.google.com/service/update2/crx?response=redirect&" + + url_loader_factory_ = profile->GetDefaultStoragePartition()->GetURLLoaderFactoryForBrowserProcess(); + extension_id_ = extension_id; + Begin(false); + } + + private: + static constexpr net::NetworkTrafficAnnotationTag kAnnotation = + net::DefineNetworkTrafficAnnotation("crest_web_store_download", R"( + semantics { + sender: "Crest Web Store install" + description: + "Downloads a Chrome Web Store extension package for installation. " + "The first request is anonymous. If the update service refuses it, " + "one retry sends the Space's cookies for that Google host." + trigger: "The person installs an extension from a Chrome Web Store listing." + data: + "The extension's ID and Chromium's version, platform and language. " + "On a retry after an authorization failure, the Space's cookies " + "for the Google update service." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: YES + cookies_store: "user" + setting: + "Only runs when the person installs an extension. Cookies are sent " + "only after the update service refuses the anonymous download." + })"); + + GURL DownloadURL() const { + return GURL("https://clients2.google.com/service/update2/crx?response=redirect&" + update_client::UpdateQueryParams::Get(update_client::UpdateQueryParams::CRX) + "&x=" + - base::EscapeQueryParamValue("id=" + extension_id + "&installsource=ondemand&uc", true)); - request->credentials_mode = network::mojom::CredentialsMode::kOmit; + base::EscapeQueryParamValue("id=" + extension_id_ + "&installsource=ondemand&uc", true)); + } + + void Begin(bool with_cookies) { + with_cookies_ = with_cookies; + length_ = -1; + auto request = std::make_unique(); + request->url = DownloadURL(); // Someone is waiting on this download, as on Chromium's own foreground // extension fetches. At the default idle priority the loader writes the // package on best-effort tasks, which Chromium holds until it counts // startup as complete: up to three minutes when no page has loaded. request->priority = net::MEDIUM; + if (with_cookies) { + // SameSite=Lax session cookies for .google.com are attached only when + // the request is same-site with the update service. + request->credentials_mode = network::mojom::CredentialsMode::kInclude; + request->site_for_cookies = net::SiteForCookies::FromUrl(request->url); + } else { + request->credentials_mode = network::mojom::CredentialsMode::kOmit; + } loader_ = network::SimpleURLLoader::Create(std::move(request), kAnnotation); loader_->SetOnResponseStartedCallback( base::BindOnce(&WebStoreDownload::Started, base::Unretained(this))); loader_->SetOnDownloadProgressCallback( base::BindRepeating(&WebStoreDownload::Progressed, base::Unretained(this))); loader_->DownloadToTempFile( - profile->GetDefaultStoragePartition()->GetURLLoaderFactoryForBrowserProcess().get(), + url_loader_factory_.get(), base::BindOnce(&WebStoreDownload::Finished, base::Unretained(this))); } - private: - static constexpr net::NetworkTrafficAnnotationTag kAnnotation = - net::DefineNetworkTrafficAnnotation("crest_web_store_download", R"( - semantics { - sender: "Crest Web Store install" - description: "Downloads a Chrome Web Store extension package for installation." - trigger: "The person installs an extension from a Chrome Web Store listing." - data: "The extension's ID and Chromium's version, platform and language." - destination: GOOGLE_OWNED_SERVICE - } - policy { - cookies_allowed: NO - setting: "Only runs when the person installs an extension." - })"); - void Started(const GURL&, const network::mojom::URLResponseHead& head) { length_ = head.content_length; } void Progressed(uint64_t received) { @@ -784,24 +819,40 @@ void Progressed(uint64_t received) { } void Finished(base::FilePath path) { - weak_factory_.InvalidateWeakPtrs(); const network::mojom::URLResponseHead* head = loader_->ResponseInfo(); const int status = head && head->headers ? head->headers->response_code() : 0; + const int net_error = loader_->NetError(); if (!path.empty() && status == 200) { + weak_factory_.InvalidateWeakPtrs(); completion_(base::apple::FilePathToNSString(path), @""); - } else { - // A successful status other than 200 carries no package: the service - // answers 204 for an extension it will not serve to this browser. - if (!path.empty()) { - base::ThreadPool::PostTask(FROM_HERE, {base::MayBlock()}, base::GetDeleteFileCallback(path)); - } - completion_(nil, status && status != 200 ? base::SysUTF8ToNSString("HTTP " + base::NumberToString(status)) - : base::SysUTF8ToNSString(net::ErrorToString(loader_->NetError()))); + delete this; + return; + } + // A successful status other than 200 carries no package: the service + // answers 204 for an extension it will not serve to this browser. + if (!path.empty()) { + base::ThreadPool::PostTask(FROM_HERE, {base::MayBlock()}, base::GetDeleteFileCallback(path)); } + // Match ExtensionDownloader::IterateFetchCredentialsAfterFailure: an + // anonymous CRX fetch that the update service refuses is retried once + // with cookies. The handle stays live so cancelling still stops the retry. + if (!with_cookies_ && + (status == net::HTTP_UNAUTHORIZED || status == net::HTTP_FORBIDDEN)) { + loader_.reset(); + progress_(0); + Begin(true); + return; + } + weak_factory_.InvalidateWeakPtrs(); + completion_(nil, status && status != 200 ? base::SysUTF8ToNSString("HTTP " + base::NumberToString(status)) + : base::SysUTF8ToNSString(net::ErrorToString(net_error))); delete this; } + scoped_refptr url_loader_factory_; std::unique_ptr loader_; + std::string extension_id_; + bool with_cookies_ = false; int64_t length_ = -1; void (^progress_)(double); void (^completion_)(NSString*, NSString*); diff --git a/CrestEngines/Chromium/README.md b/CrestEngines/Chromium/README.md index 0854b3537..4c7a993ed 100644 --- a/CrestEngines/Chromium/README.md +++ b/CrestEngines/Chromium/README.md @@ -353,9 +353,12 @@ Chromium's per-profile toolbar preferences. On a Chrome Web Store listing, the listing's own install button reads **Add to Crest** and runs Crest's install; **Install Extension…** in Site Controls does the same thing from the toolbar. Both have the Space's engine profile download -the CRX3 package from Google's update service with the request Chromium's own -Web Store installer makes, which the ungoogled baseline empties; Crest sets no -size or format limits of its own, and closing the review stops the download. +the CRX3 package from Google's update service. The request names that service +directly, because the ungoogled baseline empties Chromium's installer and +rewrites Google's hosts. Cookies stay off that request unless the service +answers 401 or 403, in which case the download is retried once with the +Space's cookies so a private or trusted-tester item can be served. Crest sets +no size or format limits of its own, and closing the review stops the download. Chromium verifies the publisher proof and expected ID before Crest shows the original install review layout. The review displays Chromium's permission warnings and offers website-access diff --git a/Documentation/ReleaseNotes.json b/Documentation/ReleaseNotes.json index 1b93bee99..cf2b803df 100644 --- a/Documentation/ReleaseNotes.json +++ b/Documentation/ReleaseNotes.json @@ -4589,6 +4589,10 @@ "website-data-locked-space-guard": { "category": "internal", "message": "Listing or clearing a locked Space's website data is refused until it is unlocked, Remove All clears every site's data in the Space, and the site commands on iPhone and iPad need a page that shows a site." + }, + "private-web-store-extension-install": { + "category": "fixed", + "message": "Private and trusted-tester extensions from the Chrome Web Store install in a Space signed in to a Google account that can see them." } } }