An .html file in a drive could only be read as source. It can now also be opened as a web page in a new tab (Theseus tab, browser tab, or the system browser from the desktop app), with the CSS, images, scripts and pages beside it loading as they would from a website. The page is served under a sandbox CSP without allow-same-origin, so its scripts run in an opaque origin: they cannot read the Pithos session or reach its API, only the files of the folder the page link covers. Links are random, last an hour from last use (12 hours at most), and refuse paths that leave the folder. Pages decrypt on the fly for drives hosted on Silent Mode.
81 lines
2.7 KiB
JavaScript
81 lines
2.7 KiB
JavaScript
// Pithos desktop: an Electron window over the same control server the web
|
|
// console runs. The server binds a random loopback port and the window signs
|
|
// in with the one-time secret, so nothing else on the machine can drive it.
|
|
|
|
const { app, BrowserWindow, shell, Menu, dialog } = require('electron');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
let pithos;
|
|
let win;
|
|
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', () => {
|
|
if (win) { if (win.isMinimized()) win.restore(); win.focus(); }
|
|
});
|
|
app.whenReady().then(start).catch((err) => {
|
|
dialog.showErrorBox('Pithos failed to start', String(err?.stack || err));
|
|
app.quit();
|
|
});
|
|
}
|
|
|
|
async function start() {
|
|
const { createPithos } = await import(pathToFileURL(path.join(__dirname, '..', 'core', 'server.js')).href);
|
|
pithos = await createPithos({
|
|
host: '127.0.0.1',
|
|
port: 0,
|
|
hostName: 'desktop',
|
|
configFile: process.env.S3D_CONFIG_FILE || undefined,
|
|
// The pinned s3d release installs here, outside the (read-only) app bundle.
|
|
binDir: path.join(app.getPath('userData'), 'bin'),
|
|
openExternal: (url) => shell.openExternal(url),
|
|
});
|
|
|
|
Menu.setApplicationMenu(process.platform === 'darwin' ? Menu.buildFromTemplate([
|
|
{ role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
|
|
]) : null);
|
|
|
|
win = new BrowserWindow({
|
|
width: 1180,
|
|
height: 800,
|
|
minWidth: 380,
|
|
minHeight: 500,
|
|
title: 'Pithos',
|
|
backgroundColor: '#0f1214',
|
|
autoHideMenuBar: true,
|
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
});
|
|
|
|
const origin = new URL(pithos.url).origin;
|
|
// Keep the window on the console; anything else goes to the system browser.
|
|
win.webContents.on('will-navigate', (e, url) => {
|
|
if (new URL(url).origin !== origin) {
|
|
e.preventDefault();
|
|
if (url.startsWith('https://')) shell.openExternal(url);
|
|
}
|
|
});
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
// HTML files opened as pages go to the system browser: the link carries
|
|
// its own ticket, and the page is sandboxed by the server.
|
|
const u = new URL(url);
|
|
if (u.origin === origin && u.pathname.startsWith('/page/')) shell.openExternal(url);
|
|
else if (url.startsWith('https://')) shell.openExternal(url);
|
|
return { action: 'deny' };
|
|
});
|
|
|
|
win.loadURL(pithos.authUrl);
|
|
win.on('closed', () => { win = null; });
|
|
}
|
|
|
|
let quitting = false;
|
|
app.on('before-quit', (e) => {
|
|
if (quitting || !pithos) return;
|
|
// Stop s3d cleanly so its upload buffer and database are flushed to disk.
|
|
e.preventDefault();
|
|
quitting = true;
|
|
pithos.close().finally(() => app.quit());
|
|
});
|
|
|
|
app.on('window-all-closed', () => app.quit());
|