mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(desktop): bundled renderer, modules and instance accounts (#3254)
This commit is contained in:
1 parent
c748c8af4e
commit
7c5fa2180a
1275 files changed
+215074
-81246
No files matched your search
@@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtempSync, readFileSync, rmSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import nodePath from 'node:path';
|
||||
import {describe, test} from 'node:test';
|
||||
import {installElectronStub} from './LocalAppTestSupport.test.mjs';
|
||||
|
||||
const noop = () => undefined;
|
||||
const ipcHandlers = new Map();
|
||||
|
||||
class ElectronEmitterStub {
|
||||
on() {
|
||||
return this;
|
||||
}
|
||||
|
||||
once() {
|
||||
return this;
|
||||
}
|
||||
|
||||
off() {
|
||||
return this;
|
||||
}
|
||||
|
||||
removeListener() {
|
||||
return this;
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
return this;
|
||||
}
|
||||
|
||||
emit() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
installElectronStub({
|
||||
app: Object.assign(new ElectronEmitterStub(), {
|
||||
isReady: () => true,
|
||||
isPackaged: false,
|
||||
getPath: () => '/tmp/fluxer-desktop-test',
|
||||
getAppPath: () => '/tmp/fluxer-desktop-test',
|
||||
getVersion: () => '0.0.0',
|
||||
getLocale: () => 'en-US',
|
||||
setPath: noop,
|
||||
exit: noop,
|
||||
quit: noop,
|
||||
whenReady: () => Promise.resolve(),
|
||||
commandLine: {appendSwitch: noop, appendArgument: noop},
|
||||
}),
|
||||
BrowserWindow: class extends ElectronEmitterStub {
|
||||
static fromWebContents() {
|
||||
return null;
|
||||
}
|
||||
|
||||
static getAllWindows() {
|
||||
return [];
|
||||
}
|
||||
|
||||
static getFocusedWindow() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
Menu: {buildFromTemplate: () => ({}), setApplicationMenu: noop},
|
||||
Notification: Object.assign(class extends ElectronEmitterStub {}, {isSupported: () => false}),
|
||||
clipboard: {},
|
||||
desktopCapturer: {},
|
||||
dialog: {showMessageBoxSync: () => 0, showErrorBox: noop},
|
||||
globalShortcut: {},
|
||||
ipcMain: Object.assign(new ElectronEmitterStub(), {
|
||||
handle: (channel, handler) => {
|
||||
ipcHandlers.set(channel, handler);
|
||||
},
|
||||
removeHandler: noop,
|
||||
}),
|
||||
nativeImage: {createFromPath: () => ({isEmpty: () => true})},
|
||||
nativeTheme: new ElectronEmitterStub(),
|
||||
net: {fetch: () => Promise.reject(new Error('outbound network is not available in tests'))},
|
||||
powerMonitor: new ElectronEmitterStub(),
|
||||
powerSaveBlocker: {start: () => 0, stop: noop, isStarted: () => false},
|
||||
protocol: {registerSchemesAsPrivileged: noop, handle: noop, unhandle: noop, isProtocolHandled: () => false},
|
||||
safeStorage: {isEncryptionAvailable: () => false},
|
||||
screen: Object.assign(new ElectronEmitterStub(), {
|
||||
getPrimaryDisplay: () => ({workAreaSize: {width: 1920, height: 1080}}),
|
||||
getAllDisplays: () => [],
|
||||
}),
|
||||
session: {defaultSession: {}},
|
||||
shell: {openExternal: noop, showItemInFolder: noop},
|
||||
systemPreferences: {},
|
||||
});
|
||||
|
||||
const {isTrustedOrigin} = await import('@electron/main/Window');
|
||||
const {DESKTOP_APP_URL} = await import('@electron/common/Constants');
|
||||
const {resolveDesktopLandingUrl} = await import('@electron/main/DesktopLastRoute');
|
||||
const {initializeDeepLinks} = await import('@electron/main/DeepLinks');
|
||||
|
||||
function readMainSource(name) {
|
||||
return readFileSync(new URL(`./${name}`, import.meta.url), 'utf8');
|
||||
}
|
||||
|
||||
function withArgv(argv, run) {
|
||||
const original = process.argv;
|
||||
process.argv = [original[0], original[1], ...argv];
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
process.argv = original;
|
||||
}
|
||||
}
|
||||
|
||||
const HOSTILE_ARGV = [
|
||||
'--app-url=https://evil.example',
|
||||
'--app-url',
|
||||
'https://evil.example',
|
||||
'--url',
|
||||
'https://evil.example/channels/@me',
|
||||
'--fluxer-instance=https://api.evil.example',
|
||||
'https://evil.example/',
|
||||
'file:///etc/passwd',
|
||||
];
|
||||
|
||||
describe('the window trust boundary is exactly the local app document origin', () => {
|
||||
test('the local app origin and its routes are trusted', () => {
|
||||
assert.equal(isTrustedOrigin(DESKTOP_APP_URL), true);
|
||||
assert.equal(isTrustedOrigin('fluxer-app://app/channels/@me'), true);
|
||||
assert.equal(isTrustedOrigin('fluxer-app://app/settings/user?tab=1#x'), true);
|
||||
});
|
||||
|
||||
test('the reserved proxy routes are never trusted documents', () => {
|
||||
assert.equal(isTrustedOrigin('fluxer-app://app/api/k/v1/users/@me'), false);
|
||||
assert.equal(isTrustedOrigin('fluxer-app://app/proxy/k?url=https://evil.example'), false);
|
||||
});
|
||||
|
||||
test('the credential, port and host bypasses are rejected', () => {
|
||||
assert.equal(isTrustedOrigin('fluxer-app://evil.example/x'), false);
|
||||
assert.equal(isTrustedOrigin('fluxer-app://u:p@app/'), false);
|
||||
assert.equal(isTrustedOrigin('fluxer-app://app:8080/'), false);
|
||||
});
|
||||
|
||||
test('an opaque origin is rejected, which is what protects the data: voice-debug window', () => {
|
||||
assert.equal(isTrustedOrigin('data:text/html,x'), false);
|
||||
assert.equal(isTrustedOrigin('about:blank'), false);
|
||||
assert.equal(isTrustedOrigin('file:///etc/passwd'), false);
|
||||
});
|
||||
|
||||
test('no remote origin is trusted any more', () => {
|
||||
assert.equal(isTrustedOrigin('https://web.fluxer.app/channels/@me'), false);
|
||||
assert.equal(isTrustedOrigin('https://web.canary.fluxer.app/'), false);
|
||||
assert.equal(isTrustedOrigin('https://evil.example/'), false);
|
||||
});
|
||||
|
||||
test('the OS deep link scheme is not a document origin', () => {
|
||||
assert.equal(isTrustedOrigin('fluxer://guild/channel'), false);
|
||||
});
|
||||
|
||||
test('an empty or absent URL is rejected', () => {
|
||||
assert.equal(isTrustedOrigin(''), false);
|
||||
assert.equal(isTrustedOrigin(undefined), false);
|
||||
assert.equal(isTrustedOrigin('not a url'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the main window only ever loads the local app document', () => {
|
||||
test('the window hands its only load to the retry loop, aimed at the landing url', () => {
|
||||
const windowSource = readMainSource('Window.ts');
|
||||
assert.doesNotMatch(windowSource, /\.loadURL\(|\.loadFile\(/);
|
||||
assert.match(
|
||||
windowSource,
|
||||
/createAppLoadRetry\(\{\s*webContents,\s*appUrl: resolveDesktopLandingUrl\(app\.getPath\('userData'\)\),/,
|
||||
);
|
||||
const retrySource = readMainSource('AppLoadRetry.ts');
|
||||
assert.equal(retrySource.match(/\.loadURL\(/g)?.length, 1);
|
||||
assert.match(retrySource, /webContents\.loadURL\(appUrl\)/);
|
||||
assert.match(retrySource, /const appUrl = options\.appUrl;/);
|
||||
});
|
||||
|
||||
test('no launch flag moves the landing url off the local app document', () => {
|
||||
const userData = mkdtempSync(nodePath.join(tmpdir(), 'fluxer-main-window-document-'));
|
||||
try {
|
||||
const landing = withArgv(HOSTILE_ARGV, () => resolveDesktopLandingUrl(userData));
|
||||
assert.equal(landing, `${DESKTOP_APP_URL}channels/@me`);
|
||||
assert.equal(isTrustedOrigin(landing), true);
|
||||
} finally {
|
||||
rmSync(userData, {recursive: true, force: true});
|
||||
}
|
||||
});
|
||||
|
||||
test('a deep link reaches the renderer as an in-app path, never as a document to load', async () => {
|
||||
assert.doesNotMatch(readMainSource('DeepLinks.ts'), /\.loadURL\(|\.loadFile\(/);
|
||||
for (const deepLink of ['fluxer://channels/@me', 'fluxer://evil.example/https://evil.example', 'fluxer://-/x']) {
|
||||
ipcHandlers.delete('get-initial-deep-link');
|
||||
withArgv([...HOSTILE_ARGV, deepLink], () => initializeDeepLinks());
|
||||
const payload = await ipcHandlers.get('get-initial-deep-link')();
|
||||
assert.equal(typeof payload, 'string', deepLink);
|
||||
assert.ok(payload.startsWith('/'), deepLink);
|
||||
assert.equal(isTrustedOrigin(new URL(payload.replace(/^\/+/u, ''), DESKTOP_APP_URL).href), true, deepLink);
|
||||
}
|
||||
});
|
||||
|
||||
test('a deep link that names a site is not forwarded as one', async () => {
|
||||
ipcHandlers.delete('get-initial-deep-link');
|
||||
withArgv(['https://evil.example/channels/@me'], () => initializeDeepLinks());
|
||||
assert.equal(await ipcHandlers.get('get-initial-deep-link')(), null);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user