build(electron): use live origin http protocol instead of file:// (#8464)

fix AF-1428
This commit is contained in:
pengx17
2024-10-15 05:42:52 +00:00
parent 9970138009
commit 3d3864fa5b
6 changed files with 55 additions and 121 deletions

View File

@@ -71,9 +71,9 @@ if (!process.env.SKIP_WEB_BUILD) {
const fullpath = path.join(affineWebOutDir, file); const fullpath = path.join(affineWebOutDir, file);
let content = await fs.readFile(fullpath, 'utf-8'); let content = await fs.readFile(fullpath, 'utf-8');
// replace # sourceMappingURL=76-6370cd185962bc89.js.map // 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) => { content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
return `# sourceMappingURL=assets://./${dir}/${p1}.map`; return `# sourceMappingURL=assets:///${dir}/${p1}.map`;
}); });
try { try {
await fs.writeFile(fullpath, content); await fs.writeFile(fullpath, content);

View File

@@ -30,5 +30,7 @@ const API_URL_MAPPING = {
internal: `https://insider.affine.pro`, internal: `https://insider.affine.pro`,
}; };
export const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
export const CLOUD_BASE_URL = export const CLOUD_BASE_URL =
process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType]; process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType];

View File

@@ -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 onboardingViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}onboarding`;
export const shellViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}shell.html`; export const shellViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}shell.html`;
export const customThemeViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}theme-editor`; export const customThemeViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}theme-editor`;

View File

@@ -2,37 +2,9 @@ import { join } from 'node:path';
import { net, protocol, session } from 'electron'; 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 { logger } from './logger';
import { isOfflineModeEnabled } from './utils'; 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 NETWORK_REQUESTS = ['/api', '/ws', '/socket.io', '/graphql'];
const webStaticDir = join(__dirname, '../resources/web-static'); const webStaticDir = join(__dirname, '../resources/web-static');
@@ -41,39 +13,56 @@ function isNetworkResource(pathname: string) {
return NETWORK_REQUESTS.some(opt => pathname.startsWith(opt)); return NETWORK_REQUESTS.some(opt => pathname.startsWith(opt));
} }
async function handleFileRequest(request: Request) { async function fetchLocalResource(request: Request) {
const clonedRequest = Object.assign(request.clone(), { 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, bypassCustomProtocolHandlers: true,
}); });
const urlObject = new URL(request.url); }
if (isNetworkResource(urlObject.pathname)) {
// just pass through (proxy) // mainly for loading sourcemap
return net.fetch( // seems handle for http/https does not work for sourcemaps
CLOUD_BASE_URL + urlObject.pathname + urlObject.search, async function handleAssetRequest(request: Request) {
clonedRequest return fetchLocalResource(request);
);
} 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);
}
} }
export function registerProtocol() { export function registerProtocol() {
protocol.handle('file', request => { const isSecure = CLOUD_BASE_URL.startsWith('https://');
return handleFileRequest(request);
});
protocol.handle('assets', request => { // do not proxy request when DEV_SERVER_URL is set (for local dev)
return handleFileRequest(request); if (!DEV_SERVER_URL) {
}); protocol.handle(isSecure ? 'https' : 'http', request => {
return handleHttpRequest(request);
});
protocol.handle('assets', request => {
return handleAssetRequest(request);
});
}
// hack for CORS // hack for CORS
// todo: should use a whitelist // todo: should use a whitelist
@@ -110,21 +99,15 @@ export function registerProtocol() {
const protocol = url.protocol; const protocol = url.protocol;
const origin = url.origin; const origin = url.origin;
const sameSite =
url.host === new URL(CLOUD_BASE_URL).host || protocol === 'file:';
// offline whitelist // 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 // 2. do not block devtools
// 3. block all other requests // 3. block all other requests
const blocked = (() => { const blocked = (() => {
if (!isOfflineModeEnabled()) { if (!isOfflineModeEnabled()) {
return false; return false;
} }
if ( if (origin === DEV_SERVER_URL && !isNetworkResource(pathname)) {
(protocol === 'file:' || origin.startsWith('http://localhost')) &&
!isNetworkResource(pathname)
) {
return false; return false;
} }
if ('devtools:' === protocol) { if ('devtools:' === protocol) {
@@ -141,19 +124,6 @@ export function registerProtocol() {
return; 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({ callback({
cancel: false, cancel: false,
requestHeaders: details.requestHeaders, requestHeaders: details.requestHeaders,

View File

@@ -3,10 +3,7 @@ import { app, shell } from 'electron';
app.on('web-contents-created', (_, contents) => { app.on('web-contents-created', (_, contents) => {
const isInternalUrl = (url: string) => { const isInternalUrl = (url: string) => {
return ( return (
(process.env.DEV_SERVER_URL && process.env.DEV_SERVER_URL && url.startsWith(process.env.DEV_SERVER_URL)
url.startsWith(process.env.DEV_SERVER_URL)) ||
url.startsWith('affine://') ||
url.startsWith('file://.')
); );
}; };
/** /**

View File

@@ -2,7 +2,6 @@ import { join } from 'node:path';
import { import {
app, app,
type CookiesSetDetails,
session, session,
type View, type View,
type WebContents, type WebContents,
@@ -28,7 +27,6 @@ import { mainWindowOrigin, shellViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process'; import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger'; import { logger } from '../logger';
import { globalStateStorage } from '../shared-storage/storage'; import { globalStateStorage } from '../shared-storage/storage';
import { parseCookie } from '../utils';
import { getCustomThemeWindow } from './custom-theme-window'; import { getCustomThemeWindow } from './custom-theme-window';
import { getMainWindow, MainWindowManager } from './main-window'; import { getMainWindow, MainWindowManager } from './main-window';
import { 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) => { getViewById = (id: string) => {
if (id === 'shell') { if (id === 'shell') {
return this.shellView; return this.shellView;
@@ -855,30 +842,6 @@ export class WebContentViewsManager {
}; };
} }
export async function setCookie(cookie: CookiesSetDetails): Promise<void>;
export async function setCookie(origin: string, cookie: string): Promise<void>;
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 // there is no proper way to listen to webContents resize event
// we will rely on window.resize event in renderer instead // we will rely on window.resize event in renderer instead
export async function handleWebContentsResize(webContents?: WebContents) { export async function handleWebContentsResize(webContents?: WebContents) {