diff --git a/shell/browser/atom_browser_client.cc b/shell/browser/atom_browser_client.cc index 88498d481f..9427cb5f34 100644 --- a/shell/browser/atom_browser_client.cc +++ b/shell/browser/atom_browser_client.cc @@ -1034,6 +1034,13 @@ void AtomBrowserClient::RegisterNonNetworkNavigationURLLoaderFactories( content::WebContents::FromFrameTreeNodeId(frame_tree_node_id); api::Protocol* protocol = api::Protocol::FromWrappedClass( v8::Isolate::GetCurrent(), web_contents->GetBrowserContext()); +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + factories->emplace( + extensions::kExtensionScheme, + extensions::CreateExtensionNavigationURLLoaderFactory( + web_contents->GetBrowserContext(), + false /* we don't support extensions::WebViewGuest */)); +#endif if (protocol) protocol->RegisterURLLoaderFactories(factories); } @@ -1042,6 +1049,13 @@ void AtomBrowserClient::RegisterNonNetworkSubresourceURLLoaderFactories( int render_process_id, int render_frame_id, NonNetworkURLLoaderFactoryMap* factories) { +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + auto factory = extensions::CreateExtensionURLLoaderFactory(render_process_id, + render_frame_id); + if (factory) + factories->emplace(extensions::kExtensionScheme, std::move(factory)); +#endif + // Chromium may call this even when NetworkService is not enabled. content::RenderFrameHost* frame_host = content::RenderFrameHost::FromID(render_process_id, render_frame_id); diff --git a/shell/browser/atom_browser_main_parts.cc b/shell/browser/atom_browser_main_parts.cc index 5234c870e3..e7ea465c18 100644 --- a/shell/browser/atom_browser_main_parts.cc +++ b/shell/browser/atom_browser_main_parts.cc @@ -226,9 +226,6 @@ AtomBrowserMainParts::AtomBrowserMainParts( electron_bindings_(new ElectronBindings(uv_default_loop())) { DCHECK(!self_) << "Cannot have two AtomBrowserMainParts"; self_ = this; - // Register extension scheme as web safe scheme. - content::ChildProcessSecurityPolicy::GetInstance()->RegisterWebSafeScheme( - "chrome-extension"); } AtomBrowserMainParts::~AtomBrowserMainParts() { diff --git a/shell/browser/browser_process_impl.cc b/shell/browser/browser_process_impl.cc index 107b1d2f3f..3f89f7b20f 100644 --- a/shell/browser/browser_process_impl.cc +++ b/shell/browser/browser_process_impl.cc @@ -17,7 +17,9 @@ #include "components/proxy_config/pref_proxy_config_tracker_impl.h" #include "components/proxy_config/proxy_config_dictionary.h" #include "components/proxy_config/proxy_config_pref_names.h" +#include "content/public/browser/child_process_security_policy.h" #include "content/public/common/content_switches.h" +#include "extensions/common/constants.h" #include "net/proxy_resolution/proxy_config.h" #include "net/proxy_resolution/proxy_config_service.h" #include "net/proxy_resolution/proxy_config_with_annotation.h" @@ -89,6 +91,10 @@ void BrowserProcessImpl::PostEarlyInitialization() { } void BrowserProcessImpl::PreCreateThreads() { + // chrome-extension:// URLs are safe to request anywhere, but may only + // commit (including in iframes) in extension processes. + content::ChildProcessSecurityPolicy::GetInstance() + ->RegisterWebSafeIsolatedScheme(extensions::kExtensionScheme, true); // Must be created before the IOThread. // Once IOThread class is no longer needed, // this can be created on first use. diff --git a/shell/browser/extensions/atom_extensions_browser_client.cc b/shell/browser/extensions/atom_extensions_browser_client.cc index 39ae733450..8b249bdceb 100644 --- a/shell/browser/extensions/atom_extensions_browser_client.cc +++ b/shell/browser/extensions/atom_extensions_browser_client.cc @@ -24,6 +24,9 @@ #include "extensions/browser/updater/null_extension_cache.h" #include "extensions/browser/url_request_util.h" #include "extensions/common/features/feature_channel.h" +#include "extensions/common/manifest_constants.h" +#include "extensions/common/manifest_url_handlers.h" +#include "services/network/public/mojom/url_loader.mojom.h" #include "shell/browser/atom_browser_client.h" #include "shell/browser/atom_browser_context.h" #include "shell/browser/browser.h" @@ -31,9 +34,6 @@ #include "shell/browser/extensions/atom_extension_host_delegate.h" #include "shell/browser/extensions/atom_extension_system_factory.h" #include "shell/browser/extensions/atom_extension_web_contents_observer.h" -// #include "shell/browser/extensions/atom_extensions_api_client.h" -// #include "shell/browser/extensions/atom_extensions_browser_api_provider.h" -#include "services/network/public/mojom/url_loader.mojom.h" #include "shell/browser/extensions/atom_navigation_ui_data.h" #include "shell/browser/extensions/electron_extensions_api_client.h" #include "shell/browser/extensions/electron_process_manager_delegate.h" @@ -139,6 +139,37 @@ void AtomExtensionsBrowserClient::LoadResourceFromResourceBundle( NOTREACHED() << "Load resources from bundles not supported."; } +namespace { +bool AllowCrossRendererResourceLoad(const GURL& url, + content::ResourceType resource_type, + ui::PageTransition page_transition, + int child_id, + bool is_incognito, + const extensions::Extension* extension, + const extensions::ExtensionSet& extensions, + const extensions::ProcessMap& process_map, + bool* allowed) { + if (extensions::url_request_util::AllowCrossRendererResourceLoad( + url, resource_type, page_transition, child_id, is_incognito, + extension, extensions, process_map, allowed)) { + return true; + } + + // If there aren't any explicitly marked web accessible resources, the + // load should be allowed only if it is by DevTools. A close approximation is + // checking if the extension contains a DevTools page. + if (extension && !extensions::ManifestURL::Get( + extension, extensions::manifest_keys::kDevToolsPage) + .is_empty()) { + *allowed = true; + return true; + } + + // Couldn't determine if the resource is allowed or not. + return false; +} +} // namespace + bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad( const GURL& url, content::ResourceType resource_type, @@ -149,7 +180,7 @@ bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad( const extensions::ExtensionSet& extensions, const extensions::ProcessMap& process_map) { bool allowed = false; - if (extensions::url_request_util::AllowCrossRendererResourceLoad( + if (::electron::AllowCrossRendererResourceLoad( url, resource_type, page_transition, child_id, is_incognito, extension, extensions, process_map, &allowed)) { return allowed; diff --git a/shell/browser/ui/inspectable_web_contents_impl.cc b/shell/browser/ui/inspectable_web_contents_impl.cc index 376fce93db..2cdb34faa6 100644 --- a/shell/browser/ui/inspectable_web_contents_impl.cc +++ b/shell/browser/ui/inspectable_web_contents_impl.cc @@ -48,6 +48,16 @@ #include "ui/display/display.h" #include "ui/display/screen.h" +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) +#include "content/public/browser/child_process_security_policy.h" +#include "content/public/browser/render_process_host.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/manifest_constants.h" +#include "extensions/common/manifest_url_handlers.h" +#include "extensions/common/permissions/permissions_data.h" +#include "shell/browser/atom_browser_context.h" +#endif + namespace electron { namespace { @@ -571,10 +581,51 @@ void InspectableWebContentsImpl::LoadCompleted() { javascript, base::NullCallback()); } +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + AddDevToolsExtensionsToClient(); +#endif + if (view_->GetDelegate()) view_->GetDelegate()->DevToolsOpened(); } +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) +void InspectableWebContentsImpl::AddDevToolsExtensionsToClient() { + // get main browser context + auto* browser_context = web_contents_->GetBrowserContext(); + const extensions::ExtensionRegistry* registry = + extensions::ExtensionRegistry::Get(browser_context); + if (!registry) + return; + + base::ListValue results; + for (auto& extension : registry->enabled_extensions()) { + auto devtools_page_url = extensions::ManifestURL::Get( + extension.get(), extensions::manifest_keys::kDevToolsPage); + if (devtools_page_url.is_empty()) + continue; + + // Each devtools extension will need to be able to run in the devtools + // process. Grant the devtools process the ability to request URLs from the + // extension. + content::ChildProcessSecurityPolicy::GetInstance()->GrantRequestOrigin( + web_contents_->GetMainFrame()->GetProcess()->GetID(), + url::Origin::Create(extension->url())); + + std::unique_ptr extension_info( + new base::DictionaryValue()); + extension_info->SetString("startPage", devtools_page_url.spec()); + extension_info->SetString("name", extension->name()); + extension_info->SetBoolean("exposeExperimentalAPIs", + extension->permissions_data()->HasAPIPermission( + extensions::APIPermission::kExperimental)); + results.Append(std::move(extension_info)); + } + + CallClientFunction("DevToolsAPI.addExtensions", &results, NULL, NULL); +} +#endif + void InspectableWebContentsImpl::SetInspectedPageBounds(const gfx::Rect& rect) { DevToolsContentsResizingStrategy strategy(rect); if (contents_resizing_strategy_.Equals(strategy)) diff --git a/shell/browser/ui/inspectable_web_contents_impl.h b/shell/browser/ui/inspectable_web_contents_impl.h index b38ed36c34..5194c7ec48 100644 --- a/shell/browser/ui/inspectable_web_contents_impl.h +++ b/shell/browser/ui/inspectable_web_contents_impl.h @@ -21,6 +21,7 @@ #include "content/public/browser/devtools_frontend_host.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_observer.h" +#include "electron/buildflags/buildflags.h" #include "shell/browser/ui/inspectable_web_contents.h" #include "ui/gfx/geometry/rect.h" @@ -193,6 +194,10 @@ class InspectableWebContentsImpl void SendMessageAck(int request_id, const base::Value* arg1); +#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) + void AddDevToolsExtensionsToClient(); +#endif + bool frontend_loaded_; scoped_refptr agent_host_; std::unique_ptr frontend_host_; diff --git a/shell/common/extensions/api/_manifest_features.json b/shell/common/extensions/api/_manifest_features.json index 96e17ec222..7fdabc8a37 100644 --- a/shell/common/extensions/api/_manifest_features.json +++ b/shell/common/extensions/api/_manifest_features.json @@ -9,6 +9,10 @@ { "content_scripts": { "channel": "stable", - "extension_types": ["extension", "legacy_packaged_app"] + "extension_types": ["extension"] + }, + "devtools_page": { + "channel": "stable", + "extension_types": ["extension"] } } diff --git a/shell/common/extensions/atom_extensions_api_provider.cc b/shell/common/extensions/atom_extensions_api_provider.cc index e77bf5869d..d345b3c26c 100644 --- a/shell/common/extensions/atom_extensions_api_provider.cc +++ b/shell/common/extensions/atom_extensions_api_provider.cc @@ -4,14 +4,69 @@ #include "shell/common/extensions/atom_extensions_api_provider.h" +#include #include +#include +#include "base/containers/span.h" +#include "base/strings/utf_string_conversions.h" #include "electron/buildflags/buildflags.h" +#include "extensions/common/alias.h" #include "extensions/common/features/json_feature_provider_source.h" - -#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) +#include "extensions/common/manifest_constants.h" +#include "extensions/common/manifest_handler.h" +#include "extensions/common/manifest_handlers/permissions_parser.h" +#include "extensions/common/manifest_url_handlers.h" +#include "extensions/common/permissions/permissions_info.h" #include "shell/common/extensions/api/manifest_features.h" -#endif + +namespace extensions { + +namespace keys = manifest_keys; +namespace errors = manifest_errors; + +// Parses the "devtools_page" manifest key. +class DevToolsPageHandler : public ManifestHandler { + public: + DevToolsPageHandler() = default; + ~DevToolsPageHandler() override = default; + + bool Parse(Extension* extension, base::string16* error) override { + std::unique_ptr manifest_url(new ManifestURL); + std::string devtools_str; + if (!extension->manifest()->GetString(keys::kDevToolsPage, &devtools_str)) { + *error = base::ASCIIToUTF16(errors::kInvalidDevToolsPage); + return false; + } + manifest_url->url_ = extension->GetResourceURL(devtools_str); + extension->SetManifestData(keys::kDevToolsPage, std::move(manifest_url)); + PermissionsParser::AddAPIPermission(extension, APIPermission::kDevtools); + return true; + } + + private: + base::span Keys() const override { + static constexpr const char* kKeys[] = {keys::kDevToolsPage}; + return kKeys; + } + + DISALLOW_COPY_AND_ASSIGN(DevToolsPageHandler); +}; + +constexpr APIPermissionInfo::InitInfo permissions_to_register[] = { + {APIPermission::kDevtools, "devtools", + APIPermissionInfo::kFlagImpliesFullURLAccess | + APIPermissionInfo::kFlagCannotBeOptional | + APIPermissionInfo::kFlagInternal}, +}; +base::span GetPermissionInfos() { + return base::make_span(permissions_to_register); +} +base::span GetPermissionAliases() { + return base::span(); +} + +} // namespace extensions namespace electron { @@ -60,8 +115,16 @@ base::StringPiece AtomExtensionsAPIProvider::GetAPISchema( } void AtomExtensionsAPIProvider::RegisterPermissions( - extensions::PermissionsInfo* permissions_info) {} + extensions::PermissionsInfo* permissions_info) { + permissions_info->RegisterPermissions(extensions::GetPermissionInfos(), + extensions::GetPermissionAliases()); +} -void AtomExtensionsAPIProvider::RegisterManifestHandlers() {} +void AtomExtensionsAPIProvider::RegisterManifestHandlers() { + extensions::ManifestHandlerRegistry* registry = + extensions::ManifestHandlerRegistry::Get(); + registry->RegisterHandler( + std::make_unique()); +} } // namespace electron diff --git a/spec-main/extensions-spec.ts b/spec-main/extensions-spec.ts index dc19ea82a0..588dfcfa45 100644 --- a/spec-main/extensions-spec.ts +++ b/spec-main/extensions-spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { session, BrowserWindow, ipcMain } from 'electron' +import { session, BrowserWindow, ipcMain, WebContents } from 'electron' import { closeAllWindows, closeWindow } from './window-helpers' import * as http from 'http' import { AddressInfo } from 'net' @@ -138,6 +138,45 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex } }) }) + + describe('devtools extensions', () => { + let showPanelTimeoutId: any = null + afterEach(() => { + if (showPanelTimeoutId) clearTimeout(showPanelTimeoutId) + }) + const showLastDevToolsPanel = (w: BrowserWindow) => { + w.webContents.once('devtools-opened', () => { + const show = () => { + if (w == null || w.isDestroyed()) return + const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined } + if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) { + return + } + + const showLastPanel = () => { + // this is executed in the devtools context, where UI is a global + const { UI } = (window as any) + const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id + UI.inspectorView.showPanel(lastPanelId) + } + devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => { + showPanelTimeoutId = setTimeout(show, 100) + }) + } + showPanelTimeoutId = setTimeout(show, 100) + }) + } + + it('loads a devtools extension', async () => { + const customSession = session.fromPartition(`persist:${require('uuid').v4()}`); + (customSession as any).loadExtension(path.join(fixtures, 'extensions', 'devtools-extension')) + const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } }) + await w.loadURL('data:text/html,hello') + w.webContents.openDevTools() + showLastDevToolsPanel(w) + await emittedOnce(ipcMain, 'winning') + }) + }) }) ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => { diff --git a/spec-main/fixtures/extensions/devtools-extension/foo.html b/spec-main/fixtures/extensions/devtools-extension/foo.html new file mode 100644 index 0000000000..d5df384f53 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/foo.html @@ -0,0 +1,9 @@ + + + + + foo + + + + diff --git a/spec-main/fixtures/extensions/devtools-extension/foo.js b/spec-main/fixtures/extensions/devtools-extension/foo.js new file mode 100644 index 0000000000..3196c3e4ab --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/foo.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line +chrome.devtools.panels.create('Foo', 'icon.png', 'index.html') diff --git a/spec-main/fixtures/extensions/devtools-extension/index.html b/spec-main/fixtures/extensions/devtools-extension/index.html new file mode 100644 index 0000000000..dde48b4c80 --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/index.html @@ -0,0 +1,5 @@ + + + a custom devtools extension + + diff --git a/spec-main/fixtures/extensions/devtools-extension/index.js b/spec-main/fixtures/extensions/devtools-extension/index.js new file mode 100644 index 0000000000..f3d98a6e0f --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/index.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line +chrome.devtools.inspectedWindow.eval(`require("electron").ipcRenderer.send("winning")`, (result, exc) => { + console.log(result, exc) +}) diff --git a/spec-main/fixtures/extensions/devtools-extension/manifest.json b/spec-main/fixtures/extensions/devtools-extension/manifest.json new file mode 100644 index 0000000000..bf5acfc24f --- /dev/null +++ b/spec-main/fixtures/extensions/devtools-extension/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "version": "1.0", + "devtools_page": "foo.html", + "manifest_version": 2 +}