mirror of
https://github.com/gethomepage/homepage.git
synced 2026-01-14 05:02:09 +08:00
Ok, truenas custom proxy
This commit is contained in:
177
src/widgets/truenas/proxy.js
Normal file
177
src/widgets/truenas/proxy.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import { sanitizeErrorURL } from "utils/proxy/api-helpers";
|
||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
import validateWidgetData from "utils/proxy/validate-widget-data";
|
||||||
|
import widgets from "widgets/widgets";
|
||||||
|
|
||||||
|
const logger = createLogger("truenasProxyHandler");
|
||||||
|
|
||||||
|
function buildWebsocketUrl(baseUrl) {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
const pathname = url.pathname.replace(/\/$/, "");
|
||||||
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
url.pathname = `${pathname}/websocket`;
|
||||||
|
url.search = "";
|
||||||
|
url.hash = "";
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForMessage(ws, matcher) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("TrueNAS websocket request timed out"));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const handleMessage = (data) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data.toString());
|
||||||
|
const matchResult = matcher(parsed);
|
||||||
|
if (matchResult !== undefined) {
|
||||||
|
cleanup();
|
||||||
|
if (matchResult instanceof Error) {
|
||||||
|
reject(matchResult);
|
||||||
|
} else {
|
||||||
|
resolve(matchResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (err) => {
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("TrueNAS websocket closed the connection"));
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.off("message", handleMessage);
|
||||||
|
ws.off("error", handleError);
|
||||||
|
ws.off("close", handleClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on("message", handleMessage);
|
||||||
|
ws.on("error", handleError);
|
||||||
|
ws.on("close", handleClose);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureConnected(ws) {
|
||||||
|
ws.send(JSON.stringify({ msg: "connect", version: "1", support: ["1"] }));
|
||||||
|
await waitForMessage(ws, (message) => (message?.msg === "connected" ? true : undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextId = 1;
|
||||||
|
async function sendMethod(ws, method, params = []) {
|
||||||
|
const id = nextId++;
|
||||||
|
ws.send(JSON.stringify({ id, msg: "method", method, params }));
|
||||||
|
|
||||||
|
return waitForMessage(ws, (message) => {
|
||||||
|
if (message?.msg === "result" && message.id === id) {
|
||||||
|
if (message.error) {
|
||||||
|
return new Error(message.error.reason || JSON.stringify(message.error));
|
||||||
|
}
|
||||||
|
return message.result;
|
||||||
|
}
|
||||||
|
if (message?.msg === "error" && message.id === id) {
|
||||||
|
return new Error(message.error || "Unknown websocket error");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate(ws, widget) {
|
||||||
|
if (widget?.key) {
|
||||||
|
try {
|
||||||
|
const apiKeyResult = await sendMethod(ws, "auth.login_with_api_key", [widget.key]);
|
||||||
|
if (apiKeyResult === true) return;
|
||||||
|
logger.warn("TrueNAS API key authentication failed, falling back to username/password when available.");
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("TrueNAS API key authentication failed: %s", err?.message ?? err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget?.username && widget?.password) {
|
||||||
|
const loginResult = await sendMethod(ws, "auth.login", [widget.username, widget.password]);
|
||||||
|
if (loginResult === true) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("TrueNAS authentication failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callWebsocket(widget, method) {
|
||||||
|
const wsUrl = buildWebsocketUrl(widget.url);
|
||||||
|
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
|
||||||
|
|
||||||
|
await waitForMessage(ws, (message) => (message === "open" ? true : undefined));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureConnected(ws);
|
||||||
|
await authenticate(ws, widget);
|
||||||
|
const result = await sendMethod(ws, method);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function truenasProxyHandler(req, res, map) {
|
||||||
|
const { group, service, endpoint, index } = req.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!endpoint) {
|
||||||
|
return res.status(204).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service, index);
|
||||||
|
const widgetVersion = Number(widget?.version ?? 1);
|
||||||
|
|
||||||
|
if (Number.isNaN(widgetVersion) || widgetVersion < 2) {
|
||||||
|
// Use legacy REST proxy for version 1
|
||||||
|
return credentialedProxyHandler(req, res, map);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetDefinition = widgets?.[widget.type];
|
||||||
|
const mappingEntry = Object.values(widgetDefinition?.mappings ?? {}).find(
|
||||||
|
(mapping) => mapping.endpoint === endpoint,
|
||||||
|
);
|
||||||
|
const wsMethod = mappingEntry?.wsMethod;
|
||||||
|
|
||||||
|
if (!wsMethod) {
|
||||||
|
logger.debug("Missing wsMethod mapping for TrueNAS endpoint %s", endpoint);
|
||||||
|
return res.status(500).json({ error: "Missing wsMethod mapping." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = await callWebsocket(widget, wsMethod);
|
||||||
|
|
||||||
|
if (!validateWidgetData(widget, endpoint, data)) {
|
||||||
|
return res.status(500).json({ error: { message: "Invalid data", url: sanitizeErrorURL(widget.url), data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map) data = map(data);
|
||||||
|
|
||||||
|
return res.status(200).json(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.status) {
|
||||||
|
return res.status(err.status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
logger.warn("Websocket call for TrueNAS failed, falling back to REST: %s", err?.message ?? err);
|
||||||
|
return credentialedProxyHandler(req, res, map);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return res.status(500).json({ error: err?.message ?? "Unexpected TrueNAS error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user