From 3d3864fa5bd4d4c3888b26d84136bf81f8d71de9 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Tue, 15 Oct 2024 05:42:52 +0000 Subject: [PATCH] build(electron): use live origin http protocol instead of file:// (#8464) fix AF-1428 --- .../apps/electron/scripts/generate-assets.ts | 4 +- .../frontend/apps/electron/src/main/config.ts | 2 + .../apps/electron/src/main/constants.ts | 4 +- .../apps/electron/src/main/protocol.ts | 124 +++++++----------- .../src/main/security-restrictions.ts | 5 +- .../src/main/windows-manager/tab-views.ts | 37 ------ 6 files changed, 55 insertions(+), 121 deletions(-) diff --git a/packages/frontend/apps/electron/scripts/generate-assets.ts b/packages/frontend/apps/electron/scripts/generate-assets.ts index f548798ccd..436114ea2c 100755 --- a/packages/frontend/apps/electron/scripts/generate-assets.ts +++ b/packages/frontend/apps/electron/scripts/generate-assets.ts @@ -71,9 +71,9 @@ if (!process.env.SKIP_WEB_BUILD) { const fullpath = path.join(affineWebOutDir, file); let content = await fs.readFile(fullpath, 'utf-8'); // replace # sourceMappingURL=76-6370cd185962bc89.js.map - // to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map + // to # sourceMappingURL=/{dir}/76-6370cd185962bc89.js.map content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => { - return `# sourceMappingURL=assets://./${dir}/${p1}.map`; + return `# sourceMappingURL=assets:///${dir}/${p1}.map`; }); try { await fs.writeFile(fullpath, content); diff --git a/packages/frontend/apps/electron/src/main/config.ts b/packages/frontend/apps/electron/src/main/config.ts index 14ef8a7c9f..e920c56298 100644 --- a/packages/frontend/apps/electron/src/main/config.ts +++ b/packages/frontend/apps/electron/src/main/config.ts @@ -30,5 +30,7 @@ const API_URL_MAPPING = { internal: `https://insider.affine.pro`, }; +export const DEV_SERVER_URL = process.env.DEV_SERVER_URL; + export const CLOUD_BASE_URL = process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType]; diff --git a/packages/frontend/apps/electron/src/main/constants.ts b/packages/frontend/apps/electron/src/main/constants.ts index 734db54f51..56b20db61d 100644 --- a/packages/frontend/apps/electron/src/main/constants.ts +++ b/packages/frontend/apps/electron/src/main/constants.ts @@ -1,4 +1,6 @@ -export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.'; +import { CLOUD_BASE_URL } from './config'; + +export const mainWindowOrigin = CLOUD_BASE_URL; export const onboardingViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}onboarding`; export const shellViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}shell.html`; export const customThemeViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}theme-editor`; diff --git a/packages/frontend/apps/electron/src/main/protocol.ts b/packages/frontend/apps/electron/src/main/protocol.ts index f186fd87a3..769ac25b03 100644 --- a/packages/frontend/apps/electron/src/main/protocol.ts +++ b/packages/frontend/apps/electron/src/main/protocol.ts @@ -2,37 +2,9 @@ import { join } from 'node:path'; import { net, protocol, session } from 'electron'; -import { CLOUD_BASE_URL } from './config'; +import { CLOUD_BASE_URL, DEV_SERVER_URL } from './config'; import { logger } from './logger'; import { isOfflineModeEnabled } from './utils'; -import { getCookies } from './windows-manager'; - -protocol.registerSchemesAsPrivileged([ - { - scheme: 'assets', - privileges: { - secure: false, - corsEnabled: true, - supportFetchAPI: true, - standard: true, - bypassCSP: true, - }, - }, -]); - -protocol.registerSchemesAsPrivileged([ - { - scheme: 'file', - privileges: { - secure: false, - corsEnabled: true, - supportFetchAPI: true, - standard: true, - bypassCSP: true, - stream: true, - }, - }, -]); const NETWORK_REQUESTS = ['/api', '/ws', '/socket.io', '/graphql']; const webStaticDir = join(__dirname, '../resources/web-static'); @@ -41,39 +13,56 @@ function isNetworkResource(pathname: string) { return NETWORK_REQUESTS.some(opt => pathname.startsWith(opt)); } -async function handleFileRequest(request: Request) { - const clonedRequest = Object.assign(request.clone(), { +async function fetchLocalResource(request: Request) { + const url = new URL(request.url); + const pathname = url.pathname; + // this will be file types (in the web-static folder) + let filepath = ''; + // if is a file type, load the file in resources + if (pathname.split('/').at(-1)?.includes('.')) { + filepath = join(webStaticDir, decodeURIComponent(pathname)); + } else { + // else, fallback to load the index.html instead + filepath = join(webStaticDir, 'index.html'); + } + return net.fetch('file://' + filepath, request); +} + +async function handleHttpRequest(request: Request) { + const url = new URL(request.url); + const pathname = url.pathname; + const sameSite = url.host === new URL(CLOUD_BASE_URL).host; + + console.log('request', request.url); + + const isStaticResource = sameSite && !isNetworkResource(pathname); + if (isStaticResource) { + return fetchLocalResource(request); + } + return net.fetch(request, { bypassCustomProtocolHandlers: true, }); - const urlObject = new URL(request.url); - if (isNetworkResource(urlObject.pathname)) { - // just pass through (proxy) - return net.fetch( - CLOUD_BASE_URL + urlObject.pathname + urlObject.search, - clonedRequest - ); - } else { - // this will be file types (in the web-static folder) - let filepath = ''; - // if is a file type, load the file in resources - if (urlObject.pathname.split('/').at(-1)?.includes('.')) { - filepath = join(webStaticDir, decodeURIComponent(urlObject.pathname)); - } else { - // else, fallback to load the index.html instead - filepath = join(webStaticDir, 'index.html'); - } - return net.fetch('file://' + filepath, clonedRequest); - } +} + +// mainly for loading sourcemap +// seems handle for http/https does not work for sourcemaps +async function handleAssetRequest(request: Request) { + return fetchLocalResource(request); } export function registerProtocol() { - protocol.handle('file', request => { - return handleFileRequest(request); - }); + const isSecure = CLOUD_BASE_URL.startsWith('https://'); - protocol.handle('assets', request => { - return handleFileRequest(request); - }); + // do not proxy request when DEV_SERVER_URL is set (for local dev) + if (!DEV_SERVER_URL) { + protocol.handle(isSecure ? 'https' : 'http', request => { + return handleHttpRequest(request); + }); + + protocol.handle('assets', request => { + return handleAssetRequest(request); + }); + } // hack for CORS // todo: should use a whitelist @@ -110,21 +99,15 @@ export function registerProtocol() { const protocol = url.protocol; const origin = url.origin; - const sameSite = - url.host === new URL(CLOUD_BASE_URL).host || protocol === 'file:'; - // offline whitelist - // 1. do not block non-api request for http://localhost || file:// (local dev assets) + // 1. do not block non-api request for DEV_SERVER_URL // 2. do not block devtools // 3. block all other requests const blocked = (() => { if (!isOfflineModeEnabled()) { return false; } - if ( - (protocol === 'file:' || origin.startsWith('http://localhost')) && - !isNetworkResource(pathname) - ) { + if (origin === DEV_SERVER_URL && !isNetworkResource(pathname)) { return false; } if ('devtools:' === protocol) { @@ -141,19 +124,6 @@ export function registerProtocol() { return; } - // session cookies are set to file:// on production - // if sending request to the cloud, attach the session cookie (to affine cloud server) - if (isNetworkResource(pathname) && sameSite) { - const cookie = getCookies(); - if (cookie) { - const cookieString = cookie.map(c => `${c.name}=${c.value}`).join('; '); - details.requestHeaders['cookie'] = cookieString; - } - - // add the referer and origin headers - details.requestHeaders['referer'] ??= CLOUD_BASE_URL; - details.requestHeaders['origin'] ??= CLOUD_BASE_URL; - } callback({ cancel: false, requestHeaders: details.requestHeaders, diff --git a/packages/frontend/apps/electron/src/main/security-restrictions.ts b/packages/frontend/apps/electron/src/main/security-restrictions.ts index c851c7e5b4..b63f43be25 100644 --- a/packages/frontend/apps/electron/src/main/security-restrictions.ts +++ b/packages/frontend/apps/electron/src/main/security-restrictions.ts @@ -3,10 +3,7 @@ import { app, shell } from 'electron'; app.on('web-contents-created', (_, contents) => { const isInternalUrl = (url: string) => { return ( - (process.env.DEV_SERVER_URL && - url.startsWith(process.env.DEV_SERVER_URL)) || - url.startsWith('affine://') || - url.startsWith('file://.') + process.env.DEV_SERVER_URL && url.startsWith(process.env.DEV_SERVER_URL) ); }; /** diff --git a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts index 0aa42c3c23..040510f7a5 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts @@ -2,7 +2,6 @@ import { join } from 'node:path'; import { app, - type CookiesSetDetails, session, type View, type WebContents, @@ -28,7 +27,6 @@ import { mainWindowOrigin, shellViewUrl } from '../constants'; import { ensureHelperProcess } from '../helper-process'; import { logger } from '../logger'; import { globalStateStorage } from '../shared-storage/storage'; -import { parseCookie } from '../utils'; import { getCustomThemeWindow } from './custom-theme-window'; import { getMainWindow, MainWindowManager } from './main-window'; import { @@ -738,17 +736,6 @@ export class WebContentViewsManager { }); }; - setCookie = async (cookiesSetDetails: CookiesSetDetails) => { - const views = this.allViews; - if (!views) { - return; - } - logger.info('setting cookie to main window view(s)', cookiesSetDetails); - for (const view of views) { - await view.webContents.session.cookies.set(cookiesSetDetails); - } - }; - getViewById = (id: string) => { if (id === 'shell') { return this.shellView; @@ -855,30 +842,6 @@ export class WebContentViewsManager { }; } -export async function setCookie(cookie: CookiesSetDetails): Promise; -export async function setCookie(origin: string, cookie: string): Promise; - -export async function setCookie( - arg0: CookiesSetDetails | string, - arg1?: string -) { - const details = - typeof arg1 === 'string' && typeof arg0 === 'string' - ? parseCookie(arg0, arg1) - : arg0; - - logger.info('setting cookie to main window', details); - - if (typeof details !== 'object') { - throw new Error('invalid cookie details'); - } - return WebContentViewsManager.instance.setCookie(details); -} - -export function getCookies() { - return WebContentViewsManager.instance.cookies; -} - // there is no proper way to listen to webContents resize event // we will rely on window.resize event in renderer instead export async function handleWebContentsResize(webContents?: WebContents) {