mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(core): desktop multiple server support (#8979)
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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') &&
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface AuthenticationRequest {
|
||||
method: 'magic-link' | 'oauth';
|
||||
payload: Record<string, any>;
|
||||
server?: string;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user