mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
v26.10.3 (#1692)
* 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>
This commit is contained in:
577 files changed
+76479
-101831
No files matched your search
@@ -0,0 +1,165 @@
|
||||
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 };
|
||||
Reference in new issue
Block a user