|
|
|
|
@@ -2,9 +2,37 @@ import { join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
import { net, protocol, session } from 'electron';
|
|
|
|
|
|
|
|
|
|
import { CLOUD_BASE_URL, DEV_SERVER_URL } from './config';
|
|
|
|
|
import { CLOUD_BASE_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');
|
|
|
|
|
@@ -13,57 +41,49 @@ function isNetworkResource(pathname: string) {
|
|
|
|
|
return NETWORK_REQUESTS.some(opt => pathname.startsWith(opt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
const isStaticResource = sameSite && !isNetworkResource(pathname);
|
|
|
|
|
if (isStaticResource) {
|
|
|
|
|
return fetchLocalResource(request);
|
|
|
|
|
}
|
|
|
|
|
return net.fetch(request, {
|
|
|
|
|
async function handleFileRequest(request: Request) {
|
|
|
|
|
const clonedRequest = Object.assign(request.clone(), {
|
|
|
|
|
bypassCustomProtocolHandlers: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mainly for loading sourcemap
|
|
|
|
|
// seems handle for http/https does not work for sourcemaps
|
|
|
|
|
async function handleAssetRequest(request: Request) {
|
|
|
|
|
return fetchLocalResource(request);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// else, fallback to load the index.html instead
|
|
|
|
|
filepath = join(webStaticDir, 'index.html');
|
|
|
|
|
}
|
|
|
|
|
return net.fetch('file://' + filepath, clonedRequest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function registerProtocol() {
|
|
|
|
|
const isSecure = CLOUD_BASE_URL.startsWith('https://');
|
|
|
|
|
protocol.handle('file', 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 handleFileRequest(request);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
protocol.handle('assets', request => {
|
|
|
|
|
return handleAssetRequest(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hack for CORS
|
|
|
|
|
// todo: should use a whitelist
|
|
|
|
|
// todo(@pengx17): remove this
|
|
|
|
|
session.defaultSession.webRequest.onHeadersReceived(
|
|
|
|
|
(responseDetails, callback) => {
|
|
|
|
|
const { responseHeaders } = responseDetails;
|
|
|
|
|
@@ -97,15 +117,21 @@ 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 DEV_SERVER_URL
|
|
|
|
|
// 1. do not block non-api request for http://localhost || file:// (local dev assets)
|
|
|
|
|
// 2. do not block devtools
|
|
|
|
|
// 3. block all other requests
|
|
|
|
|
const blocked = (() => {
|
|
|
|
|
if (!isOfflineModeEnabled()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (origin === DEV_SERVER_URL && !isNetworkResource(pathname)) {
|
|
|
|
|
if (
|
|
|
|
|
(protocol === 'file:' || origin.startsWith('http://localhost')) &&
|
|
|
|
|
!isNetworkResource(pathname)
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if ('devtools:' === protocol) {
|
|
|
|
|
@@ -122,6 +148,19 @@ 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,
|
|
|
|
|
|