fix(electron): load self-signed loopback pages
This commit is contained in:
@@ -159,7 +159,9 @@ Use an explicit override when testing a different OpenCode CLI build or when a u
|
|||||||
grants permission requests by default when no handler is set, and the panel
|
grants permission requests by default when no handler is set, and the panel
|
||||||
loads whatever address the user types. Tab favicons are fetched in this
|
loads whatever address the user types. Tab favicons are fetched in this
|
||||||
session too, so icons behind the page's own login resolve and the app's origin
|
session too, so icons behind the page's own login resolve and the app's origin
|
||||||
never requests anything from a third-party host.
|
never requests anything from a third-party host. Self-signed loopback HTTPS
|
||||||
|
pages may use an untrusted certificate authority; certificate failures for
|
||||||
|
external hosts and all other certificate errors remain blocked.
|
||||||
|
|
||||||
## IPC Pattern
|
## IPC Pattern
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
|
||||||
|
|
||||||
|
export const shouldAllowBrowserPanelCertificateError = ({ url, error }) => {
|
||||||
|
if (error !== 'net::ERR_CERT_AUTHORITY_INVALID') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.protocol === 'https:' && LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
||||||
|
|
||||||
|
test('allows untrusted certificate authorities for loopback HTTPS pages', () => {
|
||||||
|
for (const url of [
|
||||||
|
'https://localhost:58580/',
|
||||||
|
'https://127.0.0.1:58580/',
|
||||||
|
'https://[::1]:58580/',
|
||||||
|
]) {
|
||||||
|
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||||
|
url,
|
||||||
|
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps certificate validation for non-loopback pages', () => {
|
||||||
|
for (const url of [
|
||||||
|
'https://example.com/',
|
||||||
|
'https://localhost.example.com/',
|
||||||
|
'https://0.0.0.0:58580/',
|
||||||
|
]) {
|
||||||
|
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||||
|
url,
|
||||||
|
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||||
|
}), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not bypass other certificate failures or malformed URLs', () => {
|
||||||
|
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||||
|
url: 'https://localhost:58580/',
|
||||||
|
error: 'net::ERR_CERT_DATE_INVALID',
|
||||||
|
}), false);
|
||||||
|
assert.equal(shouldAllowBrowserPanelCertificateError({
|
||||||
|
url: 'not a url',
|
||||||
|
error: 'net::ERR_CERT_AUTHORITY_INVALID',
|
||||||
|
}), false);
|
||||||
|
});
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
setLinuxAutostartEnabled,
|
setLinuxAutostartEnabled,
|
||||||
} from './linux-autostart.mjs';
|
} from './linux-autostart.mjs';
|
||||||
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
|
import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs';
|
||||||
|
import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs';
|
||||||
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -1186,6 +1187,15 @@ const resolveBrowserPanelContents = (rawId) => {
|
|||||||
const hardenBrowserPanelSession = () => {
|
const hardenBrowserPanelSession = () => {
|
||||||
const panelSession = session.fromPartition(BROWSER_PANEL_PARTITION);
|
const panelSession = session.fromPartition(BROWSER_PANEL_PARTITION);
|
||||||
|
|
||||||
|
app.on('certificate-error', (event, contents, url, error, _certificate, callback) => {
|
||||||
|
if (contents.session === panelSession && shouldAllowBrowserPanelCertificateError({ url, error })) {
|
||||||
|
event.preventDefault();
|
||||||
|
callback(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(false);
|
||||||
|
});
|
||||||
|
|
||||||
panelSession.setPermissionRequestHandler((_contents, permission, callback, details) => {
|
panelSession.setPermissionRequestHandler((_contents, permission, callback, details) => {
|
||||||
log.info('[electron] browser panel denied a permission request', {
|
log.info('[electron] browser panel denied a permission request', {
|
||||||
permission,
|
permission,
|
||||||
|
|||||||
Reference in New Issue
Block a user