Files
2026-03-05 00:04:26 -06:00

189 lines
5.0 KiB
JavaScript

const hostEl = document.getElementById("plugin-host");
const frame = document.getElementById("plugin-frame");
if (!hostEl || !frame) {
console.error("plugin host not initialized");
}
const bridgeToken = hostEl?.dataset.bridgeToken || "";
const allowedPrefixes = [
"/api/clients/",
"/api/plugins",
"/api/plugins/",
];
const allowedMethods = new Set(["GET", "POST", "DELETE", "PUT", "PATCH"]);
function isAllowedUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
if (parsed.origin !== window.location.origin) return false;
return allowedPrefixes.some((prefix) => parsed.pathname.startsWith(prefix));
} catch {
return false;
}
}
function filterHeaders(headers) {
const filtered = {};
if (!headers) return filtered;
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
if (lower === "content-type" || lower === "accept") {
filtered[key] = value;
}
}
return filtered;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
window.addEventListener("message", async (event) => {
if (!event.data || event.data.type !== "plugin-fetch") return;
if (frame && event.source !== frame.contentWindow) return;
if (event.data.token !== bridgeToken) return;
const { id, request } = event.data;
if (!request || !request.url) return;
if (!allowedMethods.has(String(request.method || "GET").toUpperCase())) {
event.source?.postMessage(
{ type: "plugin-fetch-response", id, error: "method_not_allowed" },
"*",
);
return;
}
if (!isAllowedUrl(request.url)) {
event.source?.postMessage(
{ type: "plugin-fetch-response", id, error: "url_not_allowed" },
"*",
);
return;
}
try {
const res = await fetch(request.url, {
method: request.method || "GET",
headers: filterHeaders(request.headers),
body: request.body || undefined,
credentials: "include",
});
const buffer = await res.arrayBuffer();
const contentType = res.headers.get("content-type") || "";
const text = new TextDecoder().decode(buffer);
const base64 = arrayBufferToBase64(buffer);
event.source?.postMessage(
{
type: "plugin-fetch-response",
id,
ok: res.ok,
status: res.status,
statusText: res.statusText,
headers: Object.fromEntries(res.headers.entries()),
contentType,
bodyText: text,
bodyBase64: base64,
},
"*",
);
} catch (err) {
event.source?.postMessage(
{
type: "plugin-fetch-response",
id,
error: "fetch_failed",
message: String(err),
},
"*",
);
}
});
const wsSessions = new Map();
let wsSeq = 0;
function isAllowedWsUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
if (parsed.origin !== window.location.origin) return false;
return /^\/api\/clients\/[^/]+\/rd\/ws$/.test(parsed.pathname);
} catch {
return false;
}
}
window.addEventListener("message", (event) => {
if (!event.data || !event.data.type) return;
if (frame && event.source !== frame.contentWindow) return;
if (event.data.token !== bridgeToken) return;
if (event.data.type === "plugin-ws-open") {
const { url } = event.data;
if (!isAllowedWsUrl(url)) {
event.source?.postMessage(
{ type: "plugin-ws-error", error: "ws_url_not_allowed" },
"*",
);
return;
}
const id = `ws_${Date.now()}_${wsSeq++}`;
const wsUrl = url.replace("https:", "wss:").replace("http:", "ws:");
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
wsSessions.set(id, ws);
ws.addEventListener("open", () => {
event.source?.postMessage({ type: "plugin-ws-opened", id }, "*");
});
ws.addEventListener("message", (ev) => {
const payload =
ev.data instanceof ArrayBuffer
? { kind: "binary", data: arrayBufferToBase64(ev.data) }
: { kind: "text", data: String(ev.data) };
event.source?.postMessage(
{ type: "plugin-ws-message", id, payload },
"*",
);
});
ws.addEventListener("close", () => {
wsSessions.delete(id);
event.source?.postMessage({ type: "plugin-ws-closed", id }, "*");
});
ws.addEventListener("error", () => {
event.source?.postMessage({ type: "plugin-ws-error", id }, "*");
});
return;
}
if (event.data.type === "plugin-ws-send") {
const { id, data, kind } = event.data;
const ws = wsSessions.get(id);
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (kind === "binary") {
const buffer = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
ws.send(buffer);
} else {
ws.send(data);
}
return;
}
if (event.data.type === "plugin-ws-close") {
const { id } = event.data;
const ws = wsSessions.get(id);
if (ws) {
ws.close();
wsSessions.delete(id);
}
}
});