mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
* feat: shell controller, TabStage, layout and theme plugins with repair view * feat: move the classic layout into its own bundled plugin * feat: move color themes into a bundled themes plugin * fix: plugin tests and type checks work with a linked sdk * fix: home tab, layout switch and missing layout fallbacks for looks * chore: add classic layout and themes to the readme * chore: drop example plugin links from the plugin guide * feat: plugin filters by type, paged browse, hide plugins needing newer termix * feat: move desktop, status, import, audit and branding features into plugins * feat: host lock api so a plugin can load hosts from a config file (#1581) * chore: add hosts config to the plugin table * feat: login policy hook for plugins and optional https redirect (#1584) * chore: add login security to the plugin table * feat: api key socket tickets so a master can open remote sessions (#1654) * feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654) * chore: add sync to the plugin table * feat: move the sync screens and wizard out of core into the plugin (#1580) * fix: add a neutral linked desktop hook for plugins (#1580) * fix: show the linked account's name and admin flag in the app (#1580) * feat: ssh transport providers, core ssh relay and plugin folders api * feat: support FIDO2 security keys through ssh agent * feat: add companion panels beside host tabs * feat: mTLS client certificates for desktop server links * feat: native web view for plugins in the desktop app * fix: detect the desktop link window by Electron's default user agent Fixes #1675 * fix: changelog for desktop link fix * fix: keep text primary keys in the plugin data export * fix: ship sharp binaries for Windows arm64 builds * feat: client cert on the sync link types (sdk 1.0.18) * chore: update translations * feat: themed title bar for the desktop app (#1690) * chore: changelog for the themed title bar * fix: draw the title bar in the sidebar color * feat: per host layout for the terminal panel bar * chore: sdk 1.0.19 for the panel bar layout * chore: bundle all 43 plugins for 26.10.3 * chore: update translations --------- Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com> Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
166 lines
4.6 KiB
JavaScript
166 lines
4.6 KiB
JavaScript
const { isolatedUrl } = require("./isolated-window.cjs");
|
|
|
|
function cleanBounds(bounds) {
|
|
const value = (n) => Math.max(0, Math.round(Number(n) || 0));
|
|
return {
|
|
x: value(bounds?.x),
|
|
y: value(bounds?.y),
|
|
width: value(bounds?.width),
|
|
height: value(bounds?.height),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Native web views drawn inside the main window, for pages that do not work
|
|
* in a sandboxed iframe. Each one is a WebContentsView in its own
|
|
* non-persistent session, with the same limits as an isolated window: no
|
|
* permissions, no downloads, http(s) and ws(s) only. The renderer owns the
|
|
* placement and sends the bounds of the element the view should cover.
|
|
*/
|
|
function createEmbeddedViews({ WebContentsView, session, getMainWindow }) {
|
|
const views = new Map();
|
|
|
|
function destroy(id) {
|
|
const entry = views.get(id);
|
|
if (!entry) return;
|
|
views.delete(id);
|
|
const main = getMainWindow();
|
|
if (main && !main.isDestroyed()) {
|
|
try {
|
|
main.contentView.removeChildView(entry.view);
|
|
} catch {
|
|
/* already detached */
|
|
}
|
|
}
|
|
if (!entry.view.webContents.isDestroyed()) entry.view.webContents.close();
|
|
void Promise.allSettled([
|
|
entry.session.clearStorageData(),
|
|
entry.session.clearCache(),
|
|
entry.session.closeAllConnections(),
|
|
]);
|
|
}
|
|
|
|
function destroyAll() {
|
|
for (const id of [...views.keys()]) destroy(id);
|
|
}
|
|
|
|
function open(options = {}) {
|
|
const main = getMainWindow();
|
|
if (!main) throw new Error("No window to attach a view to");
|
|
const id = String(options.id || "");
|
|
if (!id) throw new Error("A view needs an id");
|
|
const url = isolatedUrl(options.url);
|
|
const existing = views.get(id);
|
|
if (existing) {
|
|
if (
|
|
existing.url === url.href &&
|
|
existing.ignoreCert === !!options.ignoreCert
|
|
) {
|
|
setBounds(id, options.bounds);
|
|
setVisible(id, options.visible !== false);
|
|
return { success: true };
|
|
}
|
|
destroy(id);
|
|
}
|
|
|
|
const isolated = session.fromPartition(
|
|
`embedded-view-${id}-${Date.now()}`,
|
|
{
|
|
cache: false,
|
|
},
|
|
);
|
|
isolated.setPermissionRequestHandler((_c, _p, callback) => callback(false));
|
|
isolated.setPermissionCheckHandler(() => false);
|
|
isolated.on("will-download", (event) => event.preventDefault());
|
|
isolated.webRequest.onBeforeRequest((details, callback) => {
|
|
const protocol = new URL(details.url).protocol;
|
|
callback({
|
|
cancel: !["http:", "https:", "ws:", "wss:"].includes(protocol),
|
|
});
|
|
});
|
|
|
|
const view = new WebContentsView({
|
|
webPreferences: {
|
|
session: isolated,
|
|
sandbox: true,
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
webviewTag: false,
|
|
webSecurity: true,
|
|
},
|
|
});
|
|
const canNavigate = (target) => {
|
|
try {
|
|
return target === "about:blank" || !!isolatedUrl(target);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
for (const name of ["will-navigate", "will-redirect"]) {
|
|
view.webContents.on(name, (event, target) => {
|
|
if (!canNavigate(target)) event.preventDefault();
|
|
});
|
|
}
|
|
view.webContents.setWindowOpenHandler(({ url: target }) =>
|
|
canNavigate(target)
|
|
? {
|
|
action: "allow",
|
|
overrideBrowserWindowOptions: {
|
|
webPreferences: { session: isolated, sandbox: true },
|
|
},
|
|
}
|
|
: { action: "deny" },
|
|
);
|
|
|
|
views.set(id, {
|
|
view,
|
|
session: isolated,
|
|
url: url.href,
|
|
origin: url.origin,
|
|
ignoreCert: !!options.ignoreCert,
|
|
});
|
|
main.contentView.addChildView(view);
|
|
setBounds(id, options.bounds);
|
|
setVisible(id, options.visible !== false);
|
|
view.webContents.loadURL(url.href).catch(() => {});
|
|
return { success: true };
|
|
}
|
|
|
|
function setBounds(id, bounds) {
|
|
const entry = views.get(id);
|
|
if (entry) entry.view.setBounds(cleanBounds(bounds));
|
|
}
|
|
|
|
function setVisible(id, visible) {
|
|
const entry = views.get(id);
|
|
if (entry) entry.view.setVisible(!!visible);
|
|
}
|
|
|
|
function handleCertificateError(event, contents, url, callback) {
|
|
for (const entry of views.values()) {
|
|
if (entry.view.webContents !== contents) continue;
|
|
event.preventDefault();
|
|
let allowed = false;
|
|
try {
|
|
allowed = entry.ignoreCert && new URL(url).origin === entry.origin;
|
|
} catch {
|
|
/* malformed URL is refused */
|
|
}
|
|
callback(allowed);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return {
|
|
open,
|
|
setBounds,
|
|
setVisible,
|
|
destroy,
|
|
destroyAll,
|
|
handleCertificateError,
|
|
};
|
|
}
|
|
|
|
module.exports = { createEmbeddedViews, cleanBounds };
|