Skip to content

Commit

Permalink
change electron's Content-Security-Policy
Browse files Browse the repository at this point in the history
  • Loading branch information
theshadowagent committed Jan 31, 2025
1 parent 2f151e3 commit 3526525
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 60 deletions.
112 changes: 100 additions & 12 deletions apps/studio/electron/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APP_NAME, APP_SCHEMA } from '@onlook/models/constants';
import { BrowserWindow, app, shell, protocol } from 'electron';
import { BrowserWindow, app, shell, protocol, session } from 'electron';
import fixPath from 'fix-path';
import { createRequire } from 'node:module';
import os from 'node:os';
Expand Down Expand Up @@ -67,10 +67,28 @@ const createWindow = () => {
titleBarStyle: 'hiddenInset',
frame: false,
webPreferences: {
webSecurity: false, // Disable for development
allowRunningInsecureContent: true,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: PRELOAD_PATH,
webviewTag: true,
},
});

// Set parent window CSP
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.webContents.insertCSS(`
meta http-equiv="Content-Security-Policy"
content="default-src 'self' onlook:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' onlook:;
script-src-elem 'self' onlook:;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src *;"
`);
});

return mainWindow;
};

Expand All @@ -80,6 +98,33 @@ const loadWindowContent = (win: BrowserWindow) => {

const initMainWindow = () => {
const win = createWindow();

// Enhanced webRequest handling
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
// console.log(`Intercepted headers for: ${details.url}`);

// Match webview-preload.js requests from any origin
if (details.url.endsWith('/webview-preload.js')) {
console.log('Modifying headers for preload script');
callback({
responseHeaders: {
...details.responseHeaders,
'Access-Control-Allow-Origin': ['*'],
'Access-Control-Allow-Headers': ['*'],
'Content-Security-Policy': [
"default-src 'self' 'unsafe-inline' data: onlook:; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' onlook:; " +
"script-src-elem 'self' onlook:; " +
'connect-src *',
],
},
});
} else {
callback({ cancel: false });
}
});

// Rest of existing init code...
win.maximize();
loadWindowContent(win);
win.webContents.setWindowOpenHandler(({ url }) => {
Expand Down Expand Up @@ -141,14 +186,51 @@ const listenForExitEvents = () => {
});
};

const checkPreloadFile = () => {
const preloadPath = path.join(__dirname, '../preload/webview.js');
console.log('Preload file exists:', fs.existsSync(preloadPath));
console.log('Preload file path:', preloadPath);
};

const configureSecurity = () => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Access-Control-Allow-Origin': ['*'],
'Access-Control-Allow-Methods': ['*'],
'Access-Control-Allow-Headers': ['*'],
},
});
});
};

const setupAppEventListeners = () => {
app.whenReady().then(() => {
// 1. First configure security headers
configureSecurity();

// 2. Then register protocol handler
protocol.handle('onlook', (request) => {
const filePath = path.join(__dirname, '../preload/webview.js');
return new Response(fs.readFileSync(filePath));
return new Response(fs.readFileSync(filePath), {
headers: {
'Content-Type': 'application/javascript',
'Content-Security-Policy':
"script-src script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' onlook:",
'Access-Control-Allow-Origin': '*',
},
});
});

// 3. Then initialize main window
checkPreloadFile();
listenForExitEvents();
initMainWindow();

// 4. Then other setup
updater.listen();
sendAnalytics('start app');
});

app.on('ready', () => {
Expand Down Expand Up @@ -195,28 +277,34 @@ const main = () => {
setupEnvironment();
configurePlatformSpecifics();

// Register onlook protocol handler for browser-compatible preload script urls
// MUST BE CALLED BEFORE APP.READY
protocol.registerSchemesAsPrivileged([
{
scheme: 'onlook',
privileges: {
standard: true,
secure: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
]);

if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
app.whenReady()
.then(() => {
configureSecurity(); // Now only handles CORS headers
setupProtocol();
setupAppEventListeners();
listenForIpcMessages();
})
.catch(console.error);

setupProtocol();
setupAppEventListeners();
listenForIpcMessages();
app.commandLine.appendSwitch('disable-web-security');
app.commandLine.appendSwitch('ignore-certificate-errors');
app.commandLine.appendSwitch('allow-running-insecure-content');
app.commandLine.appendSwitch('disable-site-isolation-trials');
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
};

main();
75 changes: 51 additions & 24 deletions apps/studio/electron/preload/webview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,50 @@ import { processDom } from './dom';
import { listenForEvents } from './events';
import cssManager from './style';

function handleBodyReady() {
setApi();
listenForEvents();
keepDomUpdated();
cssManager.injectDefaultStyles();
// Initialize API first
setApi();

// Wait for document to be ready before initializing features
function initializeFeatures() {
try {
// Initialize core features
cssManager.injectDefaultStyles();
listenForEvents();

// Set up message handler after core initialization
(window as TOnlookWindow).onlook.bridge.receive((event) => {
if (event.data.type === WebviewChannels.EXECUTE_CODE) {
const { code, messageId } = event.data;
const [port] = event.ports;

try {
const result = eval(code);
port.postMessage({ result, messageId });
} catch (error) {
port.postMessage({
error: error instanceof Error ? error.message : 'Unknown error',
messageId,
});
}
}
});

// Start DOM processing
processDom();
} catch (err) {
console.error('Error initializing features:', err);
}
}

// Use DOMContentLoaded instead of checking body
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeFeatures);
} else {
initializeFeatures();
}

console.debug('Webview preload script loaded');

let domUpdateInterval: ReturnType<typeof setInterval> | null = null;

function keepDomUpdated() {
Expand All @@ -35,34 +72,24 @@ function keepDomUpdated() {
domUpdateInterval = interval;
}

const handleDocumentBody = setInterval(() => {
const handleDocumentBody = (interval: ReturnType<typeof setInterval> | null) => {
window.onerror = function logError(errorMsg, url, lineNumber) {
console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`);
};

if (window?.document?.body) {
clearInterval(handleDocumentBody);
if (interval !== null) {
clearInterval(interval);
}
try {
handleBodyReady();
keepDomUpdated();
} catch (err) {
console.log('Error in documentBodyInit:', err);
}
}
}, 300);
};

(window as TOnlookWindow).onlook.bridge.receive((event) => {
if (event.data.type === WebviewChannels.EXECUTE_CODE) {
const { code, messageId } = event.data;
const [port] = event.ports;
// TODO: Reduce this back to 300ms
// const interval = setInterval(() => handleDocumentBody(interval), 2000);

try {
const result = eval(code);
port.postMessage({ result, messageId });
} catch (error) {
port.postMessage({
error: error instanceof Error ? error.message : 'Unknown error',
messageId,
});
}
}
});
handleDocumentBody(null);
2 changes: 1 addition & 1 deletion apps/studio/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval';"
content="script-src script-src-elem 'self' 'unsafe-inline' 'unsafe-eval' onlook:;"
/>
<title>Onlook</title>
</head>
Expand Down
64 changes: 42 additions & 22 deletions apps/studio/src/lib/editor/engine/frameview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
} from 'react';
import type { NativeImage } from 'electron';
import { WebviewChannels } from '@onlook/models/constants';
import type { TOnlookWindow } from '/electron/preload/webview/api';

export type IFrameView = HTMLIFrameElement &
Pick<
Expand Down Expand Up @@ -37,32 +36,44 @@ export const FrameView = forwardRef<IFrameView, IFrameViewProps>(({ preload, ...
const zoomLevel = useRef(1);

const handleIframeLoad = useCallback(() => {
console.log('FrameView handleIframeLoad triggered');
const iframe = iframeRef.current;
if (!iframe || !preload) {
console.error('No iframe or preload');
console.error('Missing iframe or preload path:', { iframe, preload });
return;
}

const injectPreloadScript = () => {
try {
const doc = iframe.contentDocument;
if (!doc) {
console.error('No document found');
return;
console.log('Attempting to add content load listener');

try {
// Safe cross-origin event listener
iframe.addEventListener('load', () => {
console.log('Iframe surface load event fired');
try {
if (!iframe.contentWindow?.document) {
console.error('No contentDocument available');
return;
}

console.log('Injecting preload script');
const script = iframe.contentWindow?.document.createElement('script');
if (script) {
script.src = preload!;
console.log('set script src:', script.src);

script.onerror = (e) => console.error('Preload script error:', e);
script.onload = () => console.log('Preload script loaded');

iframe.contentWindow?.document.head.appendChild(script);
} else {
console.error('Failed to create script element');
}
} catch (error) {
console.error('Injection error:', error);
}

const script = doc.createElement('script');
script.src = preload;
doc.head.appendChild(script);
} catch (error) {
console.error('Preload injection failed:', error);
}
};

if (iframe.contentDocument?.readyState === 'complete') {
injectPreloadScript();
} else {
iframe.addEventListener('load', injectPreloadScript, { once: true });
});
} catch (error) {
console.error('Failed to add load listener:', error);
}
}, [preload]);

Expand Down Expand Up @@ -209,7 +220,16 @@ export const FrameView = forwardRef<IFrameView, IFrameViewProps>(({ preload, ...
return iframe as IFrameView;
}, []);

return <iframe ref={iframeRef} {...props} onLoad={handleIframeLoad} />;
return (
<iframe
ref={iframeRef}
{...props}
onLoad={(e) => {
console.log('Native iframe load event fired');
handleIframeLoad();
}}
/>
);
});

FrameView.displayName = 'FrameView';
2 changes: 1 addition & 1 deletion apps/studio/src/routes/editor/WebviewArea/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const Frame = observer(
)}
src={settings.url}
preload="onlook://webview-preload.js"
sandbox="allow-popups allow-scripts allow-same-origin"
sandbox="allow-popups allow-scripts allow-same-origin allow-forms"
style={{
width: clampedDimensions.width,
height: clampedDimensions.height,
Expand Down

0 comments on commit 3526525

Please sign in to comment.