Files
Termix/electron/linked-server.cjs
T

119 lines
3.8 KiB
JavaScript

// The server this desktop is linked to, as the embedded backend reports it.
// The backend owns the link (and its secrets); main only keeps what it needs
// to reach that one origin: the proxy headers and basic auth a reverse proxy
// in front of it wants, whether its certificate may be self-signed, and the
// session for requests main makes itself (C2S tunnels).
let linked = null;
const WEBSOCKET_TO_HTTP = { "ws:": "http:", "wss:": "https:" };
// A WebSocket to the linked server (wss://host/...) counts as the link's
// https://host origin, so its upgrade carries the proxy headers too.
function getOrigin(url) {
try {
const parsed = new URL(url);
const httpProtocol = WEBSOCKET_TO_HTTP[parsed.protocol];
if (httpProtocol) parsed.protocol = httpProtocol;
return parsed.origin;
} catch {
return null;
}
}
function setLinkedServer(config) {
if (!config || !config.origin) {
linked = null;
return;
}
linked = {
origin: config.origin,
serverUrl: config.serverUrl || config.origin,
token: config.token || null,
headers: Array.isArray(config.headers) ? config.headers : [],
basicAuth:
config.basicAuth && config.basicAuth.username ? config.basicAuth : null,
allowInvalidCertificate: !!config.allowInvalidCertificate,
clientCert:
config.clientCert && config.clientCert.pfx ? config.clientCert : null,
};
}
function getLinkedServer() {
return linked;
}
function isLinkedOrigin(url) {
return !!linked && getOrigin(url) === linked.origin;
}
// The PFX of the link's client certificate, for TLS this process makes itself
// to the linked origin. Null for any other origin.
function clientCertOptionsFor(url) {
if (!isLinkedOrigin(url) || !linked.clientCert) return {};
return {
pfx: Buffer.from(linked.clientCert.pfx, "base64"),
passphrase: linked.clientCert.passphrase || "",
};
}
function basicAuthValue(basicAuth) {
return `Basic ${Buffer.from(
`${basicAuth.username}:${basicAuth.password || ""}`,
).toString("base64")}`;
}
// Adds the link's proxy headers to a request bound for the linked origin.
// With basic auth in front, Authorization belongs to the proxy, so a Termix
// bearer token moves into the jwt cookie, which Termix reads first.
function applyLinkHeaders(url, headers) {
if (!isLinkedOrigin(url)) return headers;
const out = { ...headers };
for (const header of linked.headers) {
if (header && header.name && header.value) out[header.name] = header.value;
}
if (linked.basicAuth) {
const authKey = Object.keys(out).find(
(key) => key.toLowerCase() === "authorization",
);
const current = authKey ? String(out[authKey]) : "";
if (authKey) delete out[authKey];
const bearer = current.match(/^Bearer\s+(.+)$/i);
if (bearer) {
const cookieKey =
Object.keys(out).find((key) => key.toLowerCase() === "cookie") ||
"Cookie";
const existing = out[cookieKey] ? `${out[cookieKey]}; ` : "";
out[cookieKey] = `${existing}jwt=${bearer[1]}`;
}
out.Authorization = basicAuthValue(linked.basicAuth);
}
return out;
}
// Headers for a request main itself makes to the linked server.
function linkedRequestHeaders(extra = {}) {
if (!linked) return { ...extra };
const base = { "X-Electron-App": "true", ...extra };
if (linked.token) base.Authorization = `Bearer ${linked.token}`;
return applyLinkHeaders(linked.origin, base);
}
// Answers a basic auth challenge from the linked origin's proxy.
function answerLogin(authInfo, url) {
if (!linked || !linked.basicAuth) return null;
if (authInfo && authInfo.isProxy) return null;
if (!isLinkedOrigin(url)) return null;
return linked.basicAuth;
}
module.exports = {
setLinkedServer,
getLinkedServer,
isLinkedOrigin,
clientCertOptionsFor,
applyLinkHeaders,
linkedRequestHeaders,
answerLogin,
};