Compare commits

...

3 Commits

Author SHA1 Message Date
Niklas Wenzel
8ce4a25520 docs: address that feature cannot be disabled again 2026-03-11 22:09:49 +01:00
Niklas Wenzel
3741b3b00a chore: remove upstreamed part from patch 2026-03-11 13:41:47 +01:00
Niklas Wenzel
6f5f6895e0 feat: make Chrome extensions work on custom protocols 2026-03-11 13:41:37 +01:00
23 changed files with 535 additions and 1 deletions

View File

@@ -1560,6 +1560,20 @@ Enables full sandbox mode on the app. This means that all renderers will be laun
This method can only be called before app is ready.
### `app.enableExtensionsOnAllProtocols()`
Enables Chrome extensions on all protocols.
By default, Chrome extensions are enabled only for a select number of protocols
such as `http`, `https`, and `file`. Calling this function will enable Chrome extensions
on all protocols, including [custom protocols](protocol.md).
This can have security implications, so enable carefully (e.g., only during development).
Once enabled, the feature cannot be disabled again until the app is restarted.
This method can only be called before app is ready.
### `app.isInApplicationsFolder()` _macOS_
Returns `boolean` - Whether the application is currently running from the

View File

@@ -148,3 +148,4 @@ fix_wayland_test_crash_on_teardown.patch
fix_set_correct_app_id_on_linux.patch
fix_pass_trigger_for_global_shortcuts_on_wayland.patch
feat_plumb_node_integration_in_worker_through_workersettings.patch
feat_allow_enabling_extensions_on_all_protocols.patch

View File

@@ -0,0 +1,149 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niklas Wenzel <dev@nikwen.de>
Date: Wed, 25 Feb 2026 16:24:03 +0100
Subject: feat: allow enabling extensions on all protocols
This allows us to use Chrome extensions on custom protocols.
The patch can't really be upstreamed, unfortunately, because there are
other URLPattern functions that we don't patch that Chrome needs.
Patching those properly would require replacing the bitmap logic in
URLPattern with a more flexible solution. This would be a larger effort
and Chromium might reject it for performance reasons.
See: https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/url_pattern.h;l=53-74;drc=50dbcddad2f8e36ddfcec21d4551f389df425c37
This patch makes it work in the context of Electron.
diff --git a/extensions/browser/api/content_settings/content_settings_helpers.cc b/extensions/browser/api/content_settings/content_settings_helpers.cc
index ea484a282d820da78e8dc1db27ad0ba6e070ac2c..6109f86f3b6ad8051473b409251bd4384d3af7e2 100644
--- a/extensions/browser/api/content_settings/content_settings_helpers.cc
+++ b/extensions/browser/api/content_settings/content_settings_helpers.cc
@@ -37,7 +37,7 @@ ContentSettingsPattern ParseExtensionPattern(std::string_view pattern_str,
std::string* error) {
const int kAllowedSchemes =
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
- URLPattern::SCHEME_FILE;
+ URLPattern::SCHEME_FILE | URLPattern::SCHEME_ELECTRON_OTHER;
URLPattern url_pattern(kAllowedSchemes);
URLPattern::ParseResult result = url_pattern.Parse(pattern_str);
if (result != URLPattern::ParseResult::kSuccess) {
diff --git a/extensions/browser/api/web_request/extension_web_request_event_router.h b/extensions/browser/api/web_request/extension_web_request_event_router.h
index 57ed3cf54b2921df09ad84906b3da7527c6080bb..29067792f0168e4df7f7aeb8114003acf4b92061 100644
--- a/extensions/browser/api/web_request/extension_web_request_event_router.h
+++ b/extensions/browser/api/web_request/extension_web_request_event_router.h
@@ -53,7 +53,8 @@ inline constexpr int kWebRequestFilterValidSchemes =
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
URLPattern::SCHEME_FTP | URLPattern::SCHEME_FILE |
URLPattern::SCHEME_EXTENSION | URLPattern::SCHEME_WS |
- URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE;
+ URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE |
+ URLPattern::SCHEME_ELECTRON_OTHER;
class WebRequestEventRouter : public KeyedService {
public:
diff --git a/extensions/common/extension.cc b/extensions/common/extension.cc
index 0e0152871689c51d4e00f39f6ad607da90e6c9be..4092c07b75c7fcaab7483dece0fe562f3d8c6906 100644
--- a/extensions/common/extension.cc
+++ b/extensions/common/extension.cc
@@ -220,7 +220,7 @@ const int Extension::kValidHostPermissionSchemes =
URLPattern::SCHEME_CHROMEUI | URLPattern::SCHEME_HTTP |
URLPattern::SCHEME_HTTPS | URLPattern::SCHEME_FILE |
URLPattern::SCHEME_FTP | URLPattern::SCHEME_WS | URLPattern::SCHEME_WSS |
- URLPattern::SCHEME_UUID_IN_PACKAGE;
+ URLPattern::SCHEME_UUID_IN_PACKAGE | URLPattern::SCHEME_ELECTRON_OTHER;
//
// Extension
diff --git a/extensions/common/url_pattern.cc b/extensions/common/url_pattern.cc
index d4328ca22fdeefd3dca88bfe959dfb849705b109..27278300ddd806c46f131f19256a381047941d9e 100644
--- a/extensions/common/url_pattern.cc
+++ b/extensions/common/url_pattern.cc
@@ -133,8 +133,18 @@ std::string_view CanonicalizeHostForMatching(std::string_view host_piece) {
} // namespace
+bool URLPattern::enable_extensions_on_all_protocols_ = false;
+
+// static
+void URLPattern::EnableExtensionsOnAllProtocols() {
+ enable_extensions_on_all_protocols_ = true;
+}
+
// static
bool URLPattern::IsValidSchemeForExtensions(std::string_view scheme) {
+ if (enable_extensions_on_all_protocols_) {
+ return true;
+ }
for (auto* valid_scheme : kValidSchemes) {
if (scheme == valid_scheme) {
return true;
@@ -145,6 +155,9 @@ bool URLPattern::IsValidSchemeForExtensions(std::string_view scheme) {
// static
int URLPattern::GetValidSchemeMaskForExtensions() {
+ if (enable_extensions_on_all_protocols_) {
+ return SCHEME_ALL;
+ }
int result = 0;
for (int valid_scheme_mask : kValidSchemeMasks) {
result |= valid_scheme_mask;
@@ -401,6 +414,10 @@ bool URLPattern::IsValidScheme(std::string_view scheme) const {
}
}
+ if (enable_extensions_on_all_protocols_) {
+ return valid_schemes_ & URLPattern::SCHEME_ELECTRON_OTHER;
+ }
+
return false;
}
diff --git a/extensions/common/url_pattern.h b/extensions/common/url_pattern.h
index 4d09251b0160644d86682ad3db7c41b50f360e6f..cda8558b176e807c8cf77900d22ba90e2f8ad3fb 100644
--- a/extensions/common/url_pattern.h
+++ b/extensions/common/url_pattern.h
@@ -64,6 +64,9 @@ class URLPattern {
SCHEME_DATA = 1 << 9,
SCHEME_UUID_IN_PACKAGE = 1 << 10,
+ // Represents all other schemes that are not mentioned above.
+ SCHEME_ELECTRON_OTHER = 1 << 11,
+
// IMPORTANT!
// SCHEME_ALL will match every scheme, including chrome://, chrome-
// extension://, about:, etc. Because this has lots of security
@@ -96,6 +99,8 @@ class URLPattern {
// Returns the mask for all schemes considered valid for extensions.
static int GetValidSchemeMaskForExtensions();
+ static void EnableExtensionsOnAllProtocols();
+
explicit URLPattern(int valid_schemes);
// Convenience to construct a URLPattern from a string. If the string is not
@@ -251,6 +256,9 @@ class URLPattern {
// Get an error string for a ParseResult.
static const char* GetParseResultString(URLPattern::ParseResult parse_result);
+ protected:
+ static bool enable_extensions_on_all_protocols_;
+
private:
// Returns true if any of the `schemes` items matches our scheme.
bool MatchesAnyScheme(const std::vector<std::string>& schemes) const;
diff --git a/extensions/common/user_script.cc b/extensions/common/user_script.cc
index f680ef4d31d580a285abe51387e3df043d4458f1..15714b79ef49a90b13e5534e25e6492e01c00447 100644
--- a/extensions/common/user_script.cc
+++ b/extensions/common/user_script.cc
@@ -69,7 +69,8 @@ enum {
kValidUserScriptSchemes = URLPattern::SCHEME_CHROMEUI |
URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS |
URLPattern::SCHEME_FILE | URLPattern::SCHEME_FTP |
- URLPattern::SCHEME_UUID_IN_PACKAGE
+ URLPattern::SCHEME_UUID_IN_PACKAGE |
+ URLPattern::SCHEME_ELECTRON_OTHER
};
// static

View File

@@ -1 +1,2 @@
chore_expose_ui_to_allow_electron_to_set_dock_side.patch
feat_allow_enabling_extension_panels_on_all_protocols.patch

View File

@@ -0,0 +1,41 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niklas Wenzel <dev@nikwen.de>
Date: Wed, 25 Feb 2026 16:23:07 +0100
Subject: feat: allow enabling extension panels on all protocols
This allows us to show Chrome extension panels on pages served over
custom protocols.
diff --git a/front_end/core/root/Runtime.ts b/front_end/core/root/Runtime.ts
index 2e212d3da32a6b3954b5e1b41f54d494268124e2..66766fefd08338ee1e5d2a1501b7be12ae3803e1 100644
--- a/front_end/core/root/Runtime.ts
+++ b/front_end/core/root/Runtime.ts
@@ -643,6 +643,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
* or guest mode, rather than a "normal" profile.
*/
isOffTheRecord: boolean,
+ devToolsExtensionsOnAllProtocols: boolean,
devToolsEnableOriginBoundCookies: HostConfigEnableOriginBoundCookies,
devToolsAnimationStylesInStylesTab: HostConfigAnimationStylesInStylesTab,
devToolsJpegXlImageFormat: HostConfigJpegXlImageFormat,
diff --git a/front_end/panels/common/ExtensionServer.ts b/front_end/panels/common/ExtensionServer.ts
index 0a5ec620b135b128013d6ddbb5299f9a5813f122..1a6118b4fa1607a634720b5579a50d1b4478b60a 100644
--- a/front_end/panels/common/ExtensionServer.ts
+++ b/front_end/panels/common/ExtensionServer.ts
@@ -12,6 +12,7 @@ import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js'; // eslint-disable-line @typescript-eslint/no-unused-vars
+import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
@@ -1607,7 +1608,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
return false;
}
- if (!kPermittedSchemes.includes(parsedURL.protocol)) {
+ if (!Root.Runtime.hostConfig.devToolsExtensionsOnAllProtocols && !kPermittedSchemes.includes(parsedURL.protocol)) {
return false;
}

View File

@@ -86,6 +86,10 @@
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-traced-handle.h"
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
#include "extensions/common/url_pattern.h"
#endif
#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
@@ -1546,6 +1550,28 @@ void App::EnableSandbox(gin_helper::ErrorThrower thrower) {
command_line->AppendSwitch(switches::kEnableSandbox);
}
void App::EnableExtensionsOnAllProtocols(gin_helper::ErrorThrower thrower) {
if (Browser::Get()->is_ready()) {
thrower.ThrowError(
"app.enableExtensionsOnAllProtocols() can only be called "
"before app is ready");
return;
}
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
enable_extensions_on_all_protocols_ = true;
URLPattern::EnableExtensionsOnAllProtocols();
#endif
}
bool App::AreExtensionsEnabledOnAllProtocols() const {
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
return enable_extensions_on_all_protocols_;
#else
return false;
#endif
}
v8::Local<v8::Promise> App::SetProxy(gin::Arguments* args) {
v8::Isolate* isolate = args->isolate();
gin_helper::Promise<void> promise(isolate);
@@ -1950,6 +1976,8 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
&App::IsHardwareAccelerationEnabled)
.SetMethod("disableDomainBlockingFor3DAPIs",
&App::DisableDomainBlockingFor3DAPIs)
.SetMethod("enableExtensionsOnAllProtocols",
&App::EnableExtensionsOnAllProtocols)
.SetMethod("getFileIcon", &App::GetFileIcon)
.SetMethod("getAppMetrics", &App::GetAppMetrics)
.SetMethod("getGPUFeatureStatus", &App::GetGPUFeatureStatus)

View File

@@ -89,6 +89,8 @@ class App final : public gin::Wrappable<App>,
static bool IsPackaged();
bool AreExtensionsEnabledOnAllProtocols() const;
App();
~App() override;
@@ -236,6 +238,7 @@ class App final : public gin::Wrappable<App>,
v8::Local<v8::Promise> GetGPUInfo(v8::Isolate* isolate,
const std::string& info_type);
void EnableSandbox(gin_helper::ErrorThrower thrower);
void EnableExtensionsOnAllProtocols(gin_helper::ErrorThrower thrower);
void SetUserAgentFallback(const std::string& user_agent);
std::string GetUserAgentFallback();
v8::Local<v8::Promise> SetProxy(gin::Arguments* args);
@@ -293,6 +296,10 @@ class App final : public gin::Wrappable<App>,
bool disable_domain_blocking_for_3DAPIs_ = false;
bool watch_singleton_socket_on_ready_ = false;
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
bool enable_extensions_on_all_protocols_ = false;
#endif
std::unique_ptr<content::ScopedAccessibilityMode> scoped_accessibility_mode_;
};

View File

@@ -617,6 +617,12 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
command_line->AppendSwitch(switches::kServiceWorkerPreload);
}
}
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
if (api::App::Get()->AreExtensionsEnabledOnAllProtocols()) {
command_line->AppendSwitch(switches::kEnableExtensionsOnAllProtocols);
}
#endif
}
}

View File

@@ -44,6 +44,7 @@
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/net/asar/asar_url_loader_factory.h"
@@ -869,6 +870,8 @@ void InspectableWebContents::GetSyncInformation(DispatchCallback callback) {
void InspectableWebContents::GetHostConfig(DispatchCallback callback) {
base::DictValue response_dict;
response_dict.Set("devToolsExtensionsOnAllProtocols",
api::App::Get()->AreExtensionsEnabledOnAllProtocols());
base::Value response = base::Value(std::move(response_dict));
std::move(callback).Run(&response);
}

View File

@@ -8,6 +8,7 @@
#include <string_view>
#include "base/strings/cstring_view.h"
#include "electron/buildflags/buildflags.h"
namespace electron {
@@ -270,6 +271,12 @@ inline constexpr base::cstring_view kStreamingSchemes = "streaming-schemes";
// Register schemes as supporting V8 code cache.
inline constexpr base::cstring_view kCodeCacheSchemes = "code-cache-schemes";
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
// Enable Chrome extensions on all protocols.
inline constexpr base::cstring_view kEnableExtensionsOnAllProtocols =
"enable-extensions-on-all-protocols";
#endif
// The browser process app model ID
inline constexpr base::cstring_view kAppUserModelId = "app-user-model-id";

View File

@@ -162,6 +162,11 @@ RendererClientBase::RendererClientBase() {
ParseSchemesCLISwitch(command_line, switches::kSecureSchemes);
for (const std::string& scheme : secure_schemes_list)
url::AddSecureScheme(scheme.data());
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
// Parse --enable-extensions-on-all-protocols
if (command_line->HasSwitch(switches::kEnableExtensionsOnAllProtocols))
URLPattern::EnableExtensionsOnAllProtocols();
#endif
// We rely on the unique process host id which is notified to the
// renderer process via command line switch from the content layer,
// if this switch is removed from the content layer for some reason,

View File

@@ -1788,6 +1788,15 @@ describe('app module', () => {
});
});
describe('enableExtensionsOnAllProtocols() API', () => {
// Proper tests are in extensions-spec.ts
it('throws when called after app is ready', () => {
expect(() => {
app.enableExtensionsOnAllProtocols();
}).to.throw(/before app is ready/);
});
});
describe('disableDomainBlockingFor3DAPIs() API', () => {
it('throws when called after app is ready', () => {
expect(() => {

View File

@@ -13,7 +13,7 @@ import * as qs from 'node:querystring';
import { ReadableStream } from 'node:stream/web';
import * as url from 'node:url';
import { listen, defer } from './lib/spec-helpers';
import { listen, defer, startRemoteControlApp } from './lib/spec-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -173,6 +173,108 @@ describe('webRequest module', () => {
expect((await ajax(`${defaultURL}exclude/test`)).data).to.equal('/exclude/test');
});
describe('with app.enableExtensionsOnAllProtocols()', () => {
it('will filter http URLs properly', async () => {
const rc = await startRemoteControlApp(['--boot-eval="app.enableExtensionsOnAllProtocols();"']);
const called = await rc.remotely(async (url: string) => {
const { BrowserWindow, session } = require('electron/main');
let called = false;
session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
called = true;
callback({ cancel: true });
});
const w = new BrowserWindow({ show: false });
await w.loadURL('about:blank');
await w.webContents.executeJavaScript(`fetch("${url}").then(() => true, () => false)`);
global.setTimeout(() => require('electron').app.quit());
return called;
}, defaultURL);
expect(called).to.be.true();
});
it('will not call webRequest.onBeforeRequest for non-custom protocol URLs that do not match the filter', async () => {
const rc = await startRemoteControlApp(['--boot-eval="app.enableExtensionsOnAllProtocols();"']);
const called = await rc.remotely(async (url: string) => {
const { BrowserWindow, session } = require('electron/main');
let called = false;
session.defaultSession.webRequest.onBeforeRequest({ urls: ['https://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
called = true;
callback({ cancel: true });
});
const w = new BrowserWindow({ show: false });
await w.loadURL('about:blank');
await w.webContents.executeJavaScript(`fetch("${url}").then(() => true, () => false)`);
global.setTimeout(() => require('electron').app.quit());
return called;
}, defaultURL);
expect(called).to.be.false();
});
// TODO(nikwen): Enable when https://github.com/electron/electron/pull/45915 has been merged.
// I have tested that it works with that patch.
it.skip('will call webRequest.onBeforeRequest for custom protocol URLs with <all_urls> filter', async () => {
const rc = await startRemoteControlApp(['--boot-eval="app.enableExtensionsOnAllProtocols();"']);
const { called, responseText } = await rc.remotely(async () => {
const { net, protocol, session } = require('electron/main');
protocol.handle('custom', () => new Response('success'));
let called = false;
session.defaultSession.webRequest.onBeforeRequest({ urls: ['<all_urls>'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
called = true;
callback({ cancel: false });
});
const response = await net.fetch('custom://app/test');
const responseText = await response.text();
global.setTimeout(() => require('electron').app.quit());
return { called, responseText };
});
expect(responseText).to.equal('success');
expect(called).to.be.true();
});
it('will not call webRequest.onBeforeRequest for custom protocol URLs that do not match the filter', async () => {
const rc = await startRemoteControlApp(['--boot-eval="app.enableExtensionsOnAllProtocols();"']);
const { called, responseText } = await rc.remotely(async () => {
const { net, protocol, session } = require('electron/main');
protocol.handle('custom', () => new Response('success'));
let called = false;
session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
called = true;
callback({ cancel: false });
});
const response = await net.fetch('custom://app/test');
const responseText = await response.text();
global.setTimeout(() => require('electron').app.quit());
return { called, responseText };
});
expect(responseText).to.equal('success');
expect(called).to.be.false();
});
});
it('receives details object', async () => {
ses.webRequest.onBeforeRequest((details, callback) => {
expect(details.id).to.be.a('number');

View File

@@ -3,6 +3,7 @@ import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extens
import { expect } from 'chai';
import * as WebSocket from 'ws';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import * as fs from 'node:fs/promises';
import * as http from 'node:http';
@@ -1338,4 +1339,26 @@ describe('chrome extensions', () => {
});
});
});
describe('custom protocol', () => {
async function runFixture (name: string) {
const appProcess = spawn(process.execPath, [(path.join(fixtures, 'extensions', name, 'main.js'))]);
let output = '';
appProcess.stdout.on('data', (data) => { output += data; });
await once(appProcess.stdout, 'end');
return output.trim();
};
it('loads DevTools extensions on custom protocols with app.enableExtensionsOnAllProtocols() and runs content and background scripts', async () => {
const output = await runFixture('custom-protocol');
expect(output).to.equal('Title: MESSAGE RECEIVED');
});
it('loads DevTools panels on custom protocols with app.enableExtensionsOnAllProtocols()', async () => {
const output = await runFixture('custom-protocol-panel');
expect(output).to.equal('ELECTRON TEST PANEL created');
});
});
});

View File

@@ -0,0 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="devtools.js"></script>
</head>
</html>

View File

@@ -0,0 +1,4 @@
/* global chrome */
chrome.devtools.panels.create('ELECTRON TEST PANEL', '', 'panel.html');
console.log('ELECTRON TEST PANEL created');

View File

@@ -0,0 +1,6 @@
{
"name": "custom-protocol-panel",
"version": "1.0",
"devtools_page": "devtools.html",
"manifest_version": 3
}

View File

@@ -0,0 +1,4 @@
<!doctype html>
<body>
DevTools panel
</body>

View File

@@ -0,0 +1,48 @@
const { app, BrowserWindow, protocol, session } = require('electron/main');
const { once } = require('node:events');
const path = require('node:path');
const html = '<html><body><h1>EMPTY PAGE</h1></body></html>';
const scheme = 'custom';
protocol.registerSchemesAsPrivileged([
{
scheme,
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
bypassCSP: false,
corsEnabled: true,
stream: true
}
}
]);
app.enableExtensionsOnAllProtocols();
app.whenReady().then(async () => {
const ses = session.defaultSession;
ses.protocol.handle(scheme, () => new Response(html, {
headers: { 'Content-Type': 'text/html' }
}));
await ses.extensions.loadExtension(path.join(__dirname, 'extension'));
const win = new BrowserWindow();
win.webContents.openDevTools();
await once(win.webContents, 'devtools-opened');
win.devToolsWebContents.on('console-message', ({ message }) => {
if (message === 'ELECTRON TEST PANEL created') {
console.log(message);
app.quit();
}
});
await win.loadURL(`${scheme}://app/`);
});

View File

@@ -0,0 +1,7 @@
/* global chrome */
chrome.runtime.onMessage.addListener((_message, sender, reply) => {
reply({
text: 'MESSAGE RECEIVED',
senderTabId: sender.tab && sender.tab.id
});
});

View File

@@ -0,0 +1,5 @@
/* global chrome */
chrome.runtime.sendMessage({ text: 'hello from content script' }, (response) => {
if (!response || !response.text) return;
document.title = response.text;
});

View File

@@ -0,0 +1,15 @@
{
"name": "custom-protocol",
"version": "1.0",
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content_script.js"],
"run_at": "document_start"
}
],
"manifest_version": 3
}

View File

@@ -0,0 +1,42 @@
const { app, BrowserWindow, protocol, session } = require('electron/main');
const path = require('node:path');
const html = '<html><body><h1>EMPTY PAGE</h1></body></html>';
const scheme = 'example';
protocol.registerSchemesAsPrivileged([
{
scheme,
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
bypassCSP: false,
corsEnabled: true,
stream: true
}
}
]);
app.enableExtensionsOnAllProtocols();
app.whenReady().then(async () => {
const ses = session.defaultSession;
ses.protocol.handle(scheme, () => new Response(html, {
headers: { 'Content-Type': 'text/html' }
}));
await ses.extensions.loadExtension(path.join(__dirname, 'extension'));
const win = new BrowserWindow();
win.on('page-title-updated', (_event, title) => {
console.log(`Title: ${title}`);
app.quit();
});
await win.loadURL(`${scheme}://app/`);
});