feat: just enough //extensions to load a simple devtools extension (#19515)

This commit is contained in:
Jeremy Apthorp 2020-01-14 16:20:30 -08:00 committed by GitHub
parent 9c1310dadc
commit 55368e4d97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 250 additions and 14 deletions

View file

@ -1034,6 +1034,13 @@ void AtomBrowserClient::RegisterNonNetworkNavigationURLLoaderFactories(
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id); content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
api::Protocol* protocol = api::Protocol::FromWrappedClass( api::Protocol* protocol = api::Protocol::FromWrappedClass(
v8::Isolate::GetCurrent(), web_contents->GetBrowserContext()); 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) if (protocol)
protocol->RegisterURLLoaderFactories(factories); protocol->RegisterURLLoaderFactories(factories);
} }
@ -1042,6 +1049,13 @@ void AtomBrowserClient::RegisterNonNetworkSubresourceURLLoaderFactories(
int render_process_id, int render_process_id,
int render_frame_id, int render_frame_id,
NonNetworkURLLoaderFactoryMap* factories) { 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. // Chromium may call this even when NetworkService is not enabled.
content::RenderFrameHost* frame_host = content::RenderFrameHost* frame_host =
content::RenderFrameHost::FromID(render_process_id, render_frame_id); content::RenderFrameHost::FromID(render_process_id, render_frame_id);

View file

@ -226,9 +226,6 @@ AtomBrowserMainParts::AtomBrowserMainParts(
electron_bindings_(new ElectronBindings(uv_default_loop())) { electron_bindings_(new ElectronBindings(uv_default_loop())) {
DCHECK(!self_) << "Cannot have two AtomBrowserMainParts"; DCHECK(!self_) << "Cannot have two AtomBrowserMainParts";
self_ = this; self_ = this;
// Register extension scheme as web safe scheme.
content::ChildProcessSecurityPolicy::GetInstance()->RegisterWebSafeScheme(
"chrome-extension");
} }
AtomBrowserMainParts::~AtomBrowserMainParts() { AtomBrowserMainParts::~AtomBrowserMainParts() {

View file

@ -17,7 +17,9 @@
#include "components/proxy_config/pref_proxy_config_tracker_impl.h" #include "components/proxy_config/pref_proxy_config_tracker_impl.h"
#include "components/proxy_config/proxy_config_dictionary.h" #include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.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 "content/public/common/content_switches.h"
#include "extensions/common/constants.h"
#include "net/proxy_resolution/proxy_config.h" #include "net/proxy_resolution/proxy_config.h"
#include "net/proxy_resolution/proxy_config_service.h" #include "net/proxy_resolution/proxy_config_service.h"
#include "net/proxy_resolution/proxy_config_with_annotation.h" #include "net/proxy_resolution/proxy_config_with_annotation.h"
@ -89,6 +91,10 @@ void BrowserProcessImpl::PostEarlyInitialization() {
} }
void BrowserProcessImpl::PreCreateThreads() { 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. // Must be created before the IOThread.
// Once IOThread class is no longer needed, // Once IOThread class is no longer needed,
// this can be created on first use. // this can be created on first use.

View file

@ -24,6 +24,9 @@
#include "extensions/browser/updater/null_extension_cache.h" #include "extensions/browser/updater/null_extension_cache.h"
#include "extensions/browser/url_request_util.h" #include "extensions/browser/url_request_util.h"
#include "extensions/common/features/feature_channel.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_client.h"
#include "shell/browser/atom_browser_context.h" #include "shell/browser/atom_browser_context.h"
#include "shell/browser/browser.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_host_delegate.h"
#include "shell/browser/extensions/atom_extension_system_factory.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_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/atom_navigation_ui_data.h"
#include "shell/browser/extensions/electron_extensions_api_client.h" #include "shell/browser/extensions/electron_extensions_api_client.h"
#include "shell/browser/extensions/electron_process_manager_delegate.h" #include "shell/browser/extensions/electron_process_manager_delegate.h"
@ -139,6 +139,37 @@ void AtomExtensionsBrowserClient::LoadResourceFromResourceBundle(
NOTREACHED() << "Load resources from bundles not supported."; 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( bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad(
const GURL& url, const GURL& url,
content::ResourceType resource_type, content::ResourceType resource_type,
@ -149,7 +180,7 @@ bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad(
const extensions::ExtensionSet& extensions, const extensions::ExtensionSet& extensions,
const extensions::ProcessMap& process_map) { const extensions::ProcessMap& process_map) {
bool allowed = false; bool allowed = false;
if (extensions::url_request_util::AllowCrossRendererResourceLoad( if (::electron::AllowCrossRendererResourceLoad(
url, resource_type, page_transition, child_id, is_incognito, url, resource_type, page_transition, child_id, is_incognito,
extension, extensions, process_map, &allowed)) { extension, extensions, process_map, &allowed)) {
return allowed; return allowed;

View file

@ -48,6 +48,16 @@
#include "ui/display/display.h" #include "ui/display/display.h"
#include "ui/display/screen.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 electron {
namespace { namespace {
@ -571,10 +581,51 @@ void InspectableWebContentsImpl::LoadCompleted() {
javascript, base::NullCallback()); javascript, base::NullCallback());
} }
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
AddDevToolsExtensionsToClient();
#endif
if (view_->GetDelegate()) if (view_->GetDelegate())
view_->GetDelegate()->DevToolsOpened(); 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<base::DictionaryValue> 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) { void InspectableWebContentsImpl::SetInspectedPageBounds(const gfx::Rect& rect) {
DevToolsContentsResizingStrategy strategy(rect); DevToolsContentsResizingStrategy strategy(rect);
if (contents_resizing_strategy_.Equals(strategy)) if (contents_resizing_strategy_.Equals(strategy))

View file

@ -21,6 +21,7 @@
#include "content/public/browser/devtools_frontend_host.h" #include "content/public/browser/devtools_frontend_host.h"
#include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "electron/buildflags/buildflags.h"
#include "shell/browser/ui/inspectable_web_contents.h" #include "shell/browser/ui/inspectable_web_contents.h"
#include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/rect.h"
@ -193,6 +194,10 @@ class InspectableWebContentsImpl
void SendMessageAck(int request_id, const base::Value* arg1); void SendMessageAck(int request_id, const base::Value* arg1);
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
void AddDevToolsExtensionsToClient();
#endif
bool frontend_loaded_; bool frontend_loaded_;
scoped_refptr<content::DevToolsAgentHost> agent_host_; scoped_refptr<content::DevToolsAgentHost> agent_host_;
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_; std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;

View file

@ -9,6 +9,10 @@
{ {
"content_scripts": { "content_scripts": {
"channel": "stable", "channel": "stable",
"extension_types": ["extension", "legacy_packaged_app"] "extension_types": ["extension"]
},
"devtools_page": {
"channel": "stable",
"extension_types": ["extension"]
} }
} }

View file

@ -4,14 +4,69 @@
#include "shell/common/extensions/atom_extensions_api_provider.h" #include "shell/common/extensions/atom_extensions_api_provider.h"
#include <memory>
#include <string> #include <string>
#include <utility>
#include "base/containers/span.h"
#include "base/strings/utf_string_conversions.h"
#include "electron/buildflags/buildflags.h" #include "electron/buildflags/buildflags.h"
#include "extensions/common/alias.h"
#include "extensions/common/features/json_feature_provider_source.h" #include "extensions/common/features/json_feature_provider_source.h"
#include "extensions/common/manifest_constants.h"
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) #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" #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<ManifestURL> 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<const char* const> 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<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
return base::make_span(permissions_to_register);
}
base::span<const Alias> GetPermissionAliases() {
return base::span<const Alias>();
}
} // namespace extensions
namespace electron { namespace electron {
@ -60,8 +115,16 @@ base::StringPiece AtomExtensionsAPIProvider::GetAPISchema(
} }
void AtomExtensionsAPIProvider::RegisterPermissions( 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<extensions::DevToolsPageHandler>());
}
} // namespace electron } // namespace electron

View file

@ -1,5 +1,5 @@
import { expect } from 'chai' import { expect } from 'chai'
import { session, BrowserWindow, ipcMain } from 'electron' import { session, BrowserWindow, ipcMain, WebContents } from 'electron'
import { closeAllWindows, closeWindow } from './window-helpers' import { closeAllWindows, closeWindow } from './window-helpers'
import * as http from 'http' import * as http from 'http'
import { AddressInfo } from 'net' 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', () => { ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>foo</title>
<!-- can't be inline, because of CSP -->
<script src="foo.js"></script>
</head>
</html>

View file

@ -0,0 +1,2 @@
// eslint-disable-next-line
chrome.devtools.panels.create('Foo', 'icon.png', 'index.html')

View file

@ -0,0 +1,5 @@
<!doctype html>
<body>
a custom devtools extension
<!-- can't be inline because of CSP -->
<script src="index.js"></script>

View file

@ -0,0 +1,4 @@
// eslint-disable-next-line
chrome.devtools.inspectedWindow.eval(`require("electron").ipcRenderer.send("winning")`, (result, exc) => {
console.log(result, exc)
})

View file

@ -0,0 +1,6 @@
{
"name": "foo",
"version": "1.0",
"devtools_page": "foo.html",
"manifest_version": 2
}