Files
horus-3d/server/gateway-proxy.js
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

309 lines
9.0 KiB
JavaScript

const { WebSocket, WebSocketServer } = require("ws");
const buildErrorResponse = (id, code, message) => {
return {
type: "res",
id,
ok: false,
error: { code, message },
};
};
const isObject = (value) => Boolean(value && typeof value === "object");
const safeJsonParse = (raw) => {
try {
return JSON.parse(raw);
} catch {
return null;
}
};
const resolvePathname = (url) => {
const raw = typeof url === "string" ? url : "";
const idx = raw.indexOf("?");
return (idx === -1 ? raw : raw.slice(0, idx)) || "/";
};
const injectAuthToken = (params, token) => {
const next = isObject(params) ? { ...params } : {};
const auth = isObject(next.auth) ? { ...next.auth } : {};
auth.token = token;
next.auth = auth;
return next;
};
const resolveOriginForUpstream = (upstreamUrl) => {
const url = new URL(upstreamUrl);
const proto = url.protocol === "wss:" ? "https:" : "http:";
const hostname =
url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "0.0.0.0"
? "localhost"
: url.hostname;
const host = url.port ? `${hostname}:${url.port}` : hostname;
return `${proto}//${host}`;
};
const hasNonEmptyToken = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.token : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasNonEmptyPassword = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.password : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasNonEmptyDeviceToken = (params) => {
const raw = params && isObject(params) && isObject(params.auth) ? params.auth.deviceToken : "";
return typeof raw === "string" && raw.trim().length > 0;
};
const hasCompleteDeviceAuth = (params) => {
const device = params && isObject(params) && isObject(params.device) ? params.device : null;
if (!device) {
return false;
}
const id = typeof device.id === "string" ? device.id.trim() : "";
const publicKey = typeof device.publicKey === "string" ? device.publicKey.trim() : "";
const signature = typeof device.signature === "string" ? device.signature.trim() : "";
const nonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
const signedAt = device.signedAt;
return (
id.length > 0 &&
publicKey.length > 0 &&
signature.length > 0 &&
nonce.length > 0 &&
Number.isFinite(signedAt) &&
signedAt >= 0
);
};
function createGatewayProxy(options) {
const {
loadUpstreamSettings,
allowWs = (req) => resolvePathname(req.url) === "/api/gateway/ws",
log = () => {},
logError = (msg, err) => console.error(msg, err),
} = options || {};
if (typeof loadUpstreamSettings !== "function") {
throw new Error("createGatewayProxy requires loadUpstreamSettings().");
}
const wss = new WebSocketServer({ noServer: true });
wss.on("connection", (browserWs) => {
let upstreamWs = null;
let upstreamReady = false;
let upstreamUrl = "";
let upstreamToken = "";
let connectRequestId = null;
let connectResponseSent = false;
let pendingConnectFrame = null;
let pendingUpstreamSetupError = null;
let closed = false;
const closeBoth = (code, reason) => {
if (closed) return;
closed = true;
try {
browserWs.close(code, reason);
} catch {}
try {
upstreamWs?.close(code, reason);
} catch {}
};
const sendToBrowser = (frame) => {
if (browserWs.readyState !== WebSocket.OPEN) return;
browserWs.send(JSON.stringify(frame));
};
const sendConnectError = (code, message) => {
if (connectRequestId && !connectResponseSent) {
connectResponseSent = true;
sendToBrowser(buildErrorResponse(connectRequestId, code, message));
}
closeBoth(1011, "connect failed");
};
const forwardConnectFrame = (frame) => {
const browserHasAuth =
hasNonEmptyToken(frame.params) ||
hasNonEmptyPassword(frame.params) ||
hasNonEmptyDeviceToken(frame.params) ||
hasCompleteDeviceAuth(frame.params);
if (!upstreamToken && !browserHasAuth) {
sendConnectError(
"studio.gateway_token_missing",
"Upstream gateway token is not configured on the Studio host."
);
return;
}
const connectFrame = browserHasAuth
? frame
: {
...frame,
params: injectAuthToken(frame.params, upstreamToken),
};
upstreamWs.send(JSON.stringify(connectFrame));
};
const maybeForwardPendingConnect = () => {
if (!pendingConnectFrame || !upstreamReady || upstreamWs?.readyState !== WebSocket.OPEN) {
return;
}
const frame = pendingConnectFrame;
pendingConnectFrame = null;
forwardConnectFrame(frame);
};
const startUpstream = async () => {
try {
const settings = await loadUpstreamSettings();
upstreamUrl = typeof settings?.url === "string" ? settings.url.trim() : "";
upstreamToken = typeof settings?.token === "string" ? settings.token.trim() : "";
} catch (err) {
logError("Failed to load upstream gateway settings.", err);
pendingUpstreamSetupError = {
code: "studio.settings_load_failed",
message: "Failed to load Studio gateway settings.",
};
return;
}
if (!upstreamUrl) {
pendingUpstreamSetupError = {
code: "studio.gateway_url_missing",
message: "Upstream gateway URL is not configured on the Studio host.",
};
return;
}
let upstreamOrigin = "";
try {
upstreamOrigin = resolveOriginForUpstream(upstreamUrl);
} catch {
pendingUpstreamSetupError = {
code: "studio.gateway_url_invalid",
message: "Upstream gateway URL is invalid on the Studio host.",
};
return;
}
upstreamWs = new WebSocket(upstreamUrl, { origin: upstreamOrigin });
upstreamWs.on("open", () => {
upstreamReady = true;
maybeForwardPendingConnect();
});
upstreamWs.on("message", (upRaw) => {
const upParsed = safeJsonParse(String(upRaw ?? ""));
if (upParsed && isObject(upParsed) && upParsed.type === "res") {
const resId = typeof upParsed.id === "string" ? upParsed.id : "";
if (resId && connectRequestId && resId === connectRequestId) {
connectResponseSent = true;
}
}
if (browserWs.readyState === WebSocket.OPEN) {
browserWs.send(String(upRaw ?? ""));
}
});
upstreamWs.on("close", (ev) => {
const reason = typeof ev?.reason === "string" ? ev.reason : "";
if (!connectResponseSent && connectRequestId) {
sendToBrowser(
buildErrorResponse(
connectRequestId,
"studio.upstream_closed",
`Upstream gateway closed (${ev.code}): ${reason}`
)
);
}
closeBoth(1012, "upstream closed");
});
upstreamWs.on("error", (err) => {
logError("Upstream gateway WebSocket error.", err);
sendConnectError(
"studio.upstream_error",
"Failed to connect to upstream gateway WebSocket."
);
});
log("proxy connected");
};
void startUpstream();
browserWs.on("message", async (raw) => {
const parsed = safeJsonParse(String(raw ?? ""));
if (!parsed || !isObject(parsed)) {
closeBoth(1003, "invalid json");
return;
}
if (!connectRequestId) {
if (parsed.type !== "req" || parsed.method !== "connect") {
closeBoth(1008, "connect required");
return;
}
const id = typeof parsed.id === "string" ? parsed.id : "";
if (!id) {
closeBoth(1008, "connect id required");
return;
}
connectRequestId = id;
if (pendingUpstreamSetupError) {
sendConnectError(pendingUpstreamSetupError.code, pendingUpstreamSetupError.message);
return;
}
pendingConnectFrame = parsed;
maybeForwardPendingConnect();
return;
}
if (!upstreamReady || upstreamWs.readyState !== WebSocket.OPEN) {
closeBoth(1013, "upstream not ready");
return;
}
if (parsed.type === "req" && parsed.method === "connect" && !connectResponseSent) {
pendingConnectFrame = null;
forwardConnectFrame(parsed);
return;
}
upstreamWs.send(JSON.stringify(parsed));
});
browserWs.on("close", () => {
closeBoth(1000, "client closed");
});
browserWs.on("error", (err) => {
logError("Browser WebSocket error.", err);
closeBoth(1011, "client error");
});
});
const handleUpgrade = (req, socket, head) => {
if (!allowWs(req)) {
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
};
return { wss, handleUpgrade };
}
module.exports = { createGatewayProxy };