feat(core): desktop multiple server support (#8979)

This commit is contained in:
EYHN
2024-12-03 05:51:09 +00:00
parent af81c95b85
commit 8963826463
137 changed files with 2052 additions and 1694 deletions

View File

@@ -22,13 +22,3 @@ export const buildType = ReleaseTypeSchema.parse(envBuildType);
export const mode = process.env.NODE_ENV;
export const isDev = mode === 'development';
const API_URL_MAPPING = {
stable: `https://app.affine.pro`,
beta: `https://insider.affine.pro`,
canary: `https://affine.fail`,
internal: `https://insider.affine.pro`,
};
export const CLOUD_BASE_URL =
process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType];

View File

@@ -84,6 +84,7 @@ async function handleAffineUrl(url: string) {
if (urlObj.hostname === 'authentication') {
const method = urlObj.searchParams.get('method');
const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false');
const server = urlObj.searchParams.get('server') || undefined;
if (
!method ||
@@ -97,6 +98,7 @@ async function handleAffineUrl(url: string) {
uiSubjects.authenticationRequest$.next({
method,
payload,
server,
});
} else if (
urlObj.searchParams.get('new-tab') &&

View File

@@ -1,11 +1,10 @@
import { join } from 'node:path';
import { net, protocol, session } from 'electron';
import cookieParser from 'set-cookie-parser';
import { CLOUD_BASE_URL } from './config';
import { logger } from './logger';
import { isOfflineModeEnabled } from './utils';
import { getCookies } from './windows-manager';
protocol.registerSchemesAsPrivileged([
{
@@ -46,32 +45,24 @@ async function handleFileRequest(request: 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('.')) {
// Sanitize pathname to prevent path traversal attacks
const decodedPath = decodeURIComponent(urlObject.pathname);
const normalizedPath = join(webStaticDir, decodedPath).normalize();
if (!normalizedPath.startsWith(webStaticDir)) {
// Attempted path traversal - reject by using empty path
filepath = join(webStaticDir, '');
} else {
filepath = normalizedPath;
}
// 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('.')) {
// Sanitize pathname to prevent path traversal attacks
const decodedPath = decodeURIComponent(urlObject.pathname);
const normalizedPath = join(webStaticDir, decodedPath).normalize();
if (!normalizedPath.startsWith(webStaticDir)) {
// Attempted path traversal - reject by using empty path
filepath = join(webStaticDir, '');
} else {
// else, fallback to load the index.html instead
filepath = join(webStaticDir, 'index.html');
filepath = normalizedPath;
}
return net.fetch('file://' + filepath, clonedRequest);
} else {
// else, fallback to load the index.html instead
filepath = join(webStaticDir, 'index.html');
}
return net.fetch('file://' + filepath, clonedRequest);
}
export function registerProtocol() {
@@ -83,31 +74,58 @@ export function registerProtocol() {
return handleFileRequest(request);
});
// todo(@pengx17): remove this
session.defaultSession.webRequest.onHeadersReceived(
(responseDetails, callback) => {
const { responseHeaders } = responseDetails;
if (responseHeaders) {
// replace SameSite=Lax with SameSite=None
const originalCookie =
responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
(async () => {
if (responseHeaders) {
const originalCookie =
responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
if (originalCookie) {
delete responseHeaders['set-cookie'];
delete responseHeaders['Set-Cookie'];
responseHeaders['Set-Cookie'] = originalCookie.map(cookie => {
let newCookie = cookie.replace(/SameSite=Lax/gi, 'SameSite=None');
// if the cookie is not secure, set it to secure
if (!newCookie.includes('Secure')) {
newCookie = newCookie + '; Secure';
if (originalCookie) {
// save the cookies, to support third party cookies
for (const cookies of originalCookie) {
const parsedCookies = cookieParser.parse(cookies);
for (const parsedCookie of parsedCookies) {
if (!parsedCookie.value) {
await session.defaultSession.cookies.remove(
responseDetails.url,
parsedCookie.name
);
} else {
await session.defaultSession.cookies.set({
url: responseDetails.url,
domain: parsedCookie.domain,
expirationDate: parsedCookie.expires?.getTime(),
httpOnly: parsedCookie.httpOnly,
secure: parsedCookie.secure,
value: parsedCookie.value,
name: parsedCookie.name,
path: parsedCookie.path,
sameSite: parsedCookie.sameSite?.toLowerCase() as
| 'unspecified'
| 'no_restriction'
| 'lax'
| 'strict'
| undefined,
});
}
}
}
return newCookie;
});
}
}
}
callback({ responseHeaders });
delete responseHeaders['access-control-allow-origin'];
delete responseHeaders['access-control-allow-headers'];
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
}
})()
.catch(err => {
logger.error('error handling headers received', err);
})
.finally(() => {
callback({ responseHeaders });
});
}
);
@@ -117,9 +135,6 @@ 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)
// 2. do not block devtools
@@ -148,22 +163,34 @@ 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;
}
(async () => {
// session cookies are set to file:// on production
// if sending request to the cloud, attach the session cookie (to affine cloud server)
if (
url.protocol === 'http:' ||
url.protocol === 'https:' ||
url.protocol === 'ws:' ||
url.protocol === 'wss:'
) {
const cookies = await session.defaultSession.cookies.get({
url: details.url,
});
// add the referer and origin headers
details.requestHeaders['referer'] ??= CLOUD_BASE_URL;
details.requestHeaders['origin'] ??= CLOUD_BASE_URL;
}
callback({
cancel: false,
requestHeaders: details.requestHeaders,
});
const cookieString = cookies
.map(c => `${c.name}=${c.value}`)
.join('; ');
delete details.requestHeaders['cookie'];
details.requestHeaders['Cookie'] = cookieString;
}
})()
.catch(err => {
logger.error('error handling before send headers', err);
})
.finally(() => {
callback({
cancel: false,
requestHeaders: details.requestHeaders,
});
});
});
}

View File

@@ -1,4 +1,5 @@
export interface AuthenticationRequest {
method: 'magic-link' | 'oauth';
payload: Record<string, any>;
server?: string;
}

View File

@@ -5,7 +5,6 @@ import {
BrowserWindow,
Menu,
MenuItem,
session,
type View,
type WebContents,
WebContentsView,
@@ -26,7 +25,7 @@ import {
import { isMacOS } from '../../shared/utils';
import { beforeAppQuit } from '../cleanup';
import { CLOUD_BASE_URL, isDev } from '../config';
import { isDev } from '../config';
import { mainWindowOrigin, shellViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger';
@@ -722,23 +721,6 @@ export class WebContentViewsManager {
// add shell view
this.createAndAddView('shell').catch(err => logger.error(err));
(async () => {
const updateCookies = () => {
session.defaultSession.cookies
.get({
url: CLOUD_BASE_URL,
})
.then(cookies => {
this.cookies = cookies;
})
.catch(err => {
logger.error('failed to get cookies', err);
});
};
updateCookies();
session.defaultSession.cookies.on('changed', () => {
updateCookies();
});
if (this.tabViewsMeta.workbenches.length === 0) {
// create a default view (e.g., on first launch)
await this.addTab();
@@ -921,10 +903,6 @@ export class WebContentViewsManager {
};
}
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) {