mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: harden plugin api for separate repos, signed updates and generic external login
This commit is contained in:
1 parent
fb9c168b6b
commit
80944301dd
310 files changed
+6577
-4927
No files matched your search
+32
-10
@@ -35,6 +35,7 @@ const IPC = {
|
||||
DOWNLOAD: "local-transfer:download",
|
||||
CANCEL: "local-transfer:cancel",
|
||||
PROGRESS: "local-transfer:progress",
|
||||
SET_API: "local-transfer:set-api",
|
||||
};
|
||||
|
||||
const READ_CHUNK_BYTES = 1024 * 1024;
|
||||
@@ -325,10 +326,19 @@ const DEVICE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
||||
// A compact JWT (base64url segments joined by dots); anything else is refused.
|
||||
const AUTH_TOKEN_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
||||
|
||||
// The file-manager plugin serves both routes under /plugin-api/file-manager
|
||||
// on the main backend.
|
||||
const FILE_MANAGER_API = "/plugin-api/file-manager";
|
||||
const DEFAULT_LOCAL_FILE_MANAGER_BASE = `http://localhost:30001${FILE_MANAGER_API}`;
|
||||
// The plugin that serves the two streaming routes tells us its /plugin-api/
|
||||
// path, so nothing here names it. Only a /plugin-api/<id> path is accepted,
|
||||
// which keeps every transfer on the backend's plugin routes.
|
||||
const API_PATH_PATTERN = /^\/plugin-api\/[a-z0-9][a-z0-9-]*$/;
|
||||
const LOCAL_BACKEND = "http://localhost:30001";
|
||||
let registeredApiPath = null;
|
||||
|
||||
function setTransferApiPath(apiPath) {
|
||||
if (typeof apiPath !== "string" || !API_PATH_PATTERN.test(apiPath)) {
|
||||
throw new Error("Invalid transfer API path");
|
||||
}
|
||||
registeredApiPath = apiPath;
|
||||
}
|
||||
|
||||
function normalizeHttpBase(candidate, what) {
|
||||
let parsed;
|
||||
@@ -346,10 +356,13 @@ function normalizeHttpBase(candidate, what) {
|
||||
// Resolves where a transfer may go. Only the two file-manager streaming
|
||||
// routes are reachable, and only on the embedded backend or the linked
|
||||
// server; credentials come from the main process, not the caller.
|
||||
function createTargetResolver({
|
||||
localBaseUrl = DEFAULT_LOCAL_FILE_MANAGER_BASE,
|
||||
getLinkedServer,
|
||||
}) {
|
||||
function createTargetResolver({ localBaseUrl, getLinkedServer, apiPath }) {
|
||||
const resolveApiPath = () => {
|
||||
const resolved = apiPath ?? registeredApiPath;
|
||||
if (!resolved)
|
||||
throw new Error("Local file transfers are not available yet");
|
||||
return resolved;
|
||||
};
|
||||
return function resolveTransferTarget({
|
||||
origin,
|
||||
route,
|
||||
@@ -389,7 +402,10 @@ function createTargetResolver({
|
||||
headers.Authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
return {
|
||||
url: `${normalizeHttpBase(localBaseUrl, "Local backend URL")}${routePath}`,
|
||||
url: `${normalizeHttpBase(
|
||||
localBaseUrl ?? `${LOCAL_BACKEND}${resolveApiPath()}`,
|
||||
"Local backend URL",
|
||||
)}${routePath}`,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
@@ -402,7 +418,7 @@ function createTargetResolver({
|
||||
}
|
||||
const base = normalizeHttpBase(linked.serverUrl, "Linked server URL");
|
||||
if (linked.token) headers.Authorization = `Bearer ${linked.token}`;
|
||||
return { url: `${base}${FILE_MANAGER_API}${routePath}`, headers };
|
||||
return { url: `${base}${resolveApiPath()}${routePath}`, headers };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown transfer origin: ${String(origin)}`);
|
||||
@@ -890,6 +906,11 @@ function createLocalFileHandlers({
|
||||
};
|
||||
|
||||
return {
|
||||
[IPC.SET_API]: wrap(async (_event, apiPath) => {
|
||||
setTransferApiPath(apiPath);
|
||||
return {};
|
||||
}),
|
||||
|
||||
[IPC.HOME]: wrap(async () => ({
|
||||
home: os.homedir(),
|
||||
separator: path.sep,
|
||||
@@ -1028,6 +1049,7 @@ module.exports = {
|
||||
// exported for tests / reuse
|
||||
createLocalFileHandlers,
|
||||
createTargetResolver,
|
||||
setTransferApiPath,
|
||||
publishDownload,
|
||||
defaultPublishFs,
|
||||
assertWithinRoot,
|
||||
|
||||
+32
-5
@@ -630,7 +630,14 @@ const isolatedWindows = createIsolatedWindows({
|
||||
// ctx.desktop.openIsolatedWindow is the one caller today.
|
||||
const BACKEND_REQUEST_HANDLERS = {
|
||||
"open-isolated-window": (payload) => isolatedWindows.open(payload),
|
||||
"launch-native-rdp": (payload) => launchNativeRdp(payload),
|
||||
// The OS client for a protocol. Only RDP has one wired up.
|
||||
"launch-external-client": (payload) =>
|
||||
payload?.protocol === "rdp"
|
||||
? launchNativeRdp(payload)
|
||||
: {
|
||||
success: false,
|
||||
error: `No external client for ${payload?.protocol ?? "this protocol"}`,
|
||||
},
|
||||
"sync-proxy-config": (payload) => {
|
||||
linkedServer.setLinkedServer(payload);
|
||||
return { success: true };
|
||||
@@ -1590,9 +1597,9 @@ ipcMain.handle("get-embedded-server-status", () => {
|
||||
};
|
||||
});
|
||||
|
||||
// OIDC System Browser Authentication (RFC 8252)
|
||||
// External login through the system browser (RFC 8252)
|
||||
ipcMain.handle(
|
||||
"oidc-system-browser-auth",
|
||||
"external-browser-login",
|
||||
async (_event, authUrl, callbackPort) => {
|
||||
const http = require("http");
|
||||
|
||||
@@ -1666,7 +1673,7 @@ ipcMain.handle(
|
||||
// Timeout after 5 minutes
|
||||
timeout = setTimeout(
|
||||
() => {
|
||||
fail(new Error("OIDC authentication timed out"));
|
||||
fail(new Error("External login timed out"));
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
@@ -1868,8 +1875,28 @@ function getC2SRemoteBaseUrl() {
|
||||
return serverUrl.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
// The socket path on the linked server that relays C2S streams. The plugin
|
||||
// that owns the relay tells us where it is, so nothing here names it.
|
||||
let c2sRelayPath = null;
|
||||
const C2S_RELAY_PATH_PATTERN =
|
||||
/^\/plugin-ws\/[a-z0-9][a-z0-9-]*\/[A-Za-z0-9/_-]+$/;
|
||||
|
||||
ipcMain.handle("set-c2s-relay-path", (_event, relayPath) => {
|
||||
if (
|
||||
typeof relayPath !== "string" ||
|
||||
!C2S_RELAY_PATH_PATTERN.test(relayPath)
|
||||
) {
|
||||
return { success: false, error: "Invalid relay path" };
|
||||
}
|
||||
c2sRelayPath = relayPath;
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
function getC2SRelayUrl() {
|
||||
const relayHttpUrl = `${getC2SRemoteBaseUrl()}/plugin-ws/tunnels/c2s/stream`;
|
||||
if (!c2sRelayPath) {
|
||||
throw new Error("Client tunnels are not available yet. Try again shortly.");
|
||||
}
|
||||
const relayHttpUrl = `${getC2SRemoteBaseUrl()}${c2sRelayPath}`;
|
||||
return relayHttpUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -53,6 +53,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
},
|
||||
startC2SAutoStartTunnels: () =>
|
||||
ipcRenderer.invoke("start-c2s-autostart-tunnels"),
|
||||
setC2SRelayPath: (relayPath) =>
|
||||
ipcRenderer.invoke("set-c2s-relay-path", relayPath),
|
||||
|
||||
onCloseActiveTab: (callback) => {
|
||||
const listener = () => callback();
|
||||
@@ -72,8 +74,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
timeoutMs,
|
||||
),
|
||||
|
||||
externalBrowserLogin: (authUrl, callbackPort) =>
|
||||
ipcRenderer.invoke("external-browser-login", authUrl, callbackPort),
|
||||
// 2.8 name, read by older server login pages. Remove in 3.0.0.
|
||||
oidcSystemBrowserAuth: (authUrl, callbackPort) =>
|
||||
ipcRenderer.invoke("oidc-system-browser-auth", authUrl, callbackPort),
|
||||
ipcRenderer.invoke("external-browser-login", authUrl, callbackPort),
|
||||
|
||||
openExternalEditor: (fileData) =>
|
||||
ipcRenderer.invoke("open-external-editor", fileData),
|
||||
@@ -151,6 +156,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
open: (targetPath) => ipcRenderer.invoke("local-fs:open", targetPath),
|
||||
},
|
||||
localTransfer: {
|
||||
setApiPath: (apiPath) =>
|
||||
ipcRenderer.invoke("local-transfer:set-api", apiPath),
|
||||
upload: (options) => ipcRenderer.invoke("local-transfer:upload", options),
|
||||
download: (options) =>
|
||||
ipcRenderer.invoke("local-transfer:download", options),
|
||||
|
||||
Reference in new issue
Block a user