mirror of
https://github.com/gethomepage/homepage.git
synced 2026-01-11 11:32:09 +08:00
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Repository Maintenance / Stale (push) Has been cancelled
Repository Maintenance / Lock Old Threads (push) Has been cancelled
Repository Maintenance / Close Answered Discussions (push) Has been cancelled
Repository Maintenance / Close Outdated Discussions (push) Has been cancelled
Repository Maintenance / Close Unsupported Feature Requests (push) Has been cancelled
173 lines
5.8 KiB
JavaScript
173 lines
5.8 KiB
JavaScript
import WebSocket from "ws";
|
|
|
|
import getServiceWidget from "utils/config/service-helpers";
|
|
import createLogger from "utils/logger";
|
|
import { formatApiCall, 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 waitForEvent(ws, handler, { event = "message", parseJson = true } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error("TrueNAS websocket wait timed out"));
|
|
}, 10000);
|
|
|
|
const handleEvent = (payload) => {
|
|
try {
|
|
let parsed = payload;
|
|
if (parseJson) {
|
|
if (Buffer.isBuffer(payload)) {
|
|
parsed = JSON.parse(payload.toString());
|
|
} else if (typeof payload === "string") {
|
|
parsed = JSON.parse(payload);
|
|
}
|
|
logger.info("Received TrueNAS websocket message: %o", parsed);
|
|
} else {
|
|
logger.info("Received TrueNAS websocket message: %o", payload);
|
|
}
|
|
const handlerResult = handler(parsed);
|
|
if (handlerResult !== undefined) {
|
|
cleanup();
|
|
if (handlerResult instanceof Error) {
|
|
reject(handlerResult);
|
|
} else {
|
|
resolve(handlerResult);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
cleanup();
|
|
reject(err);
|
|
}
|
|
};
|
|
|
|
const handleError = (err) => {
|
|
cleanup();
|
|
logger.error("TrueNAS websocket error: %s", err?.message ?? err);
|
|
reject(err);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
cleanup();
|
|
logger.error("TrueNAS websocket connection closed unexpectedly");
|
|
reject(new Error("TrueNAS websocket closed the connection"));
|
|
};
|
|
|
|
function cleanup() {
|
|
clearTimeout(timeout);
|
|
ws.off(event, handleEvent);
|
|
ws.off("error", handleError);
|
|
ws.off("close", handleClose);
|
|
}
|
|
|
|
ws.on(event, handleEvent);
|
|
ws.on("error", handleError);
|
|
ws.on("close", handleClose);
|
|
});
|
|
}
|
|
|
|
let nextId = 1;
|
|
async function sendMethod(ws, method, params = []) {
|
|
const id = nextId++;
|
|
const payload = { jsonrpc: "2.0", id, method, params };
|
|
logger.info("Sending TrueNAS websocket method %s with id %d", method, id);
|
|
ws.send(JSON.stringify(payload));
|
|
|
|
return waitForEvent(ws, (message) => {
|
|
if (message?.id !== id) return undefined;
|
|
if (message?.error) {
|
|
return new Error(message.error?.message || JSON.stringify(message.error));
|
|
}
|
|
return message?.result ?? message;
|
|
});
|
|
}
|
|
|
|
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;
|
|
logger.warn("TrueNAS username/password authentication failed.");
|
|
}
|
|
|
|
throw new Error("TrueNAS authentication failed");
|
|
}
|
|
|
|
export default async function truenasProxyHandler(req, res, map) {
|
|
const { group, service, endpoint, index } = req.query;
|
|
if (!group || !service) {
|
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
|
}
|
|
|
|
const widget = await getServiceWidget(group, service, index);
|
|
|
|
if (!widget) {
|
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
|
}
|
|
|
|
if (!endpoint) {
|
|
return res.status(204).end();
|
|
}
|
|
|
|
const version = Number(widget.version ?? 1);
|
|
if (Number.isNaN(version) || version < 2) {
|
|
// Use legacy REST proxy for version 1
|
|
return credentialedProxyHandler(req, res, map);
|
|
}
|
|
|
|
const mappingEntry = Object.values(widgets[widget.type].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;
|
|
const wsUrl = new URL(formatApiCall(widgets[widget.type].wsAPI, { ...widget }));
|
|
const useSecure = wsUrl.protocol === "https:" || Boolean(widget.key); // API key requires secure connection
|
|
if (useSecure && wsUrl.protocol !== "https:")
|
|
logger.info("Upgrading TrueNAS websocket connection to secure wss://");
|
|
wsUrl.protocol = useSecure ? "wss:" : "ws:";
|
|
logger.info("Connecting to TrueNAS websocket at %s", wsUrl);
|
|
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
|
|
await waitForEvent(ws, () => true, { event: "open", parseJson: false }); // wait for open
|
|
logger.info("Connected to TrueNAS websocket at %s", wsUrl);
|
|
try {
|
|
await authenticate(ws, widget);
|
|
data = await sendMethod(ws, wsMethod);
|
|
} finally {
|
|
ws.close();
|
|
}
|
|
|
|
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: %s", err?.message ?? err);
|
|
return res.status(500).json({ error: err?.message ?? "TrueNAS websocket call failed" });
|
|
}
|
|
}
|