change how plugins are sent to clients (https is better than whatever tf I can do)

This commit is contained in:
KingKDot
2026-05-03 22:37:08 -05:00
parent d7c05af1a3
commit 8a7217cda2
7 changed files with 256 additions and 166 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ export const CORS_HEADERS = {
};
export const SECURITY_HEADERS = {
"Content-Security-Policy": "default-src 'self'; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self'; connect-src 'self'; object-src blob:; frame-ancestors 'none'",
"Content-Security-Policy": "default-src 'self'; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://cdn.discordapp.com https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self'; connect-src 'self'; object-src blob:; frame-ancestors 'none'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
@@ -233,13 +233,19 @@ export async function listPluginManifests(
}
}
export type PluginBundle = {
manifest: PluginManifest;
binaryPath: string | null;
size: number;
};
export async function loadPluginBundle(
pluginRoot: string,
pluginId: string,
ensureExtracted: (pluginId: string) => Promise<void>,
clientOS?: string,
clientArch?: string,
): Promise<{ manifest: PluginManifest; binary: Uint8Array | null }> {
): Promise<PluginBundle> {
await ensureExtracted(pluginId);
const dir = path.join(pluginRoot, pluginId);
const manifestPath = path.join(dir, "manifest.json");
@@ -250,7 +256,7 @@ export async function loadPluginBundle(
const hasBinaries = manifest.binaries && Object.keys(manifest.binaries).length > 0;
if (!hasBinaries) {
return { manifest, binary: null };
return { manifest, binaryPath: null, size: 0 };
}
let binaryPath: string | null = null;
@@ -291,52 +297,66 @@ export async function loadPluginBundle(
binaryPath = path.join(dir, found);
}
const binary = new Uint8Array(await fs.readFile(binaryPath));
return { manifest, binary };
const stat = await fs.stat(binaryPath);
return { manifest, binaryPath, size: stat.size };
}
export type PluginPull = {
id: string;
clientId: string;
pluginId: string;
binaryPath: string;
size: number;
expiresAt: number;
timeout: ReturnType<typeof setTimeout>;
};
const PLUGIN_PULL_TTL_MS = 5 * 60_000;
const pluginPulls = new Map<string, PluginPull>();
export function getPluginPull(id: string): PluginPull | undefined {
return pluginPulls.get(id);
}
export function deletePluginPull(id: string): void {
const pull = pluginPulls.get(id);
if (!pull) return;
pluginPulls.delete(id);
clearTimeout(pull.timeout);
}
export function sendPluginBundle(
target: ClientInfo,
bundle: { manifest: PluginManifest; binary: Uint8Array | null },
bundle: PluginBundle,
): void {
if (!bundle.binary) return;
const chunkSize = 16 * 1024;
const data = bundle.binary;
const totalChunks = Math.ceil(data.length / chunkSize);
const initPayload = {
manifest: bundle.manifest,
size: data.length,
chunks: totalChunks,
};
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_init",
id: uuidv4(),
payload: initPayload,
}),
);
if (!bundle.binaryPath) return;
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, data.length);
const chunk = data.slice(start, end);
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_chunk",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id, index: i, data: chunk },
}),
);
}
const pullId = uuidv4();
const expiresAt = Date.now() + PLUGIN_PULL_TTL_MS;
const timeout = setTimeout(() => {
pluginPulls.delete(pullId);
}, PLUGIN_PULL_TTL_MS);
pluginPulls.set(pullId, {
id: pullId,
clientId: target.id,
pluginId: bundle.manifest.id,
binaryPath: bundle.binaryPath,
size: bundle.size,
expiresAt,
timeout,
});
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_load_finish",
commandType: "plugin_load_http",
id: uuidv4(),
payload: { pluginId: bundle.manifest.id },
payload: {
manifest: bundle.manifest,
size: bundle.size,
url: `/api/plugins/pull/${encodeURIComponent(pullId)}`,
},
}),
);
}
@@ -348,7 +368,7 @@ export async function dispatchAutoLoadPlugins(
isPluginLoading: (clientId: string, pluginId: string) => boolean,
markPluginLoading: (clientId: string, pluginId: string) => void,
enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void,
loadBundle: (pluginId: string, clientOS?: string, clientArch?: string) => Promise<{ manifest: PluginManifest; binary: Uint8Array | null }>,
loadBundle: (pluginId: string, clientOS?: string, clientArch?: string) => Promise<PluginBundle>,
): Promise<void> {
const autoLoadIds = Object.entries(pluginState.autoLoad)
.filter(([id, enabled]) => enabled && pluginState.enabled[id] !== false)
@@ -12,6 +12,9 @@ import { encodeMessage, type PluginSignatureInfo } from "../../protocol";
import { getConfig, updatePluginsConfig } from "../../config";
import { getOrVerifySignature, BUILTIN_TRUSTED_KEYS } from "../plugin-signature";
import type { PluginRuntime } from "../plugin-runtime/runtime";
import { getPluginPull, deletePluginPull } from "../plugin-state-bundle";
import { isAuthorizedAgentRequest } from "../agent-auth";
import { logger } from "../../logger";
type PluginManifest = {
id: string;
@@ -22,7 +25,8 @@ type PluginManifest = {
type PluginBundle = {
manifest: PluginManifest;
binary: Uint8Array | null;
binaryPath: string | null;
size: number;
};
type PluginState = {
@@ -68,6 +72,48 @@ export async function handlePluginRoutes(
return null;
}
if (req.method === "GET" && url.pathname.startsWith("/api/plugins/pull/")) {
const agentToken = getConfig().auth.agentToken;
if (!isAuthorizedAgentRequest(req, url, agentToken)) {
return new Response("Unauthorized", { status: 401 });
}
let pullId = "";
try {
pullId = decodeURIComponent(url.pathname.slice("/api/plugins/pull/".length));
} catch {
return new Response("Bad request", { status: 400 });
}
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(pullId)) {
return new Response("Bad request", { status: 400 });
}
const pull = getPluginPull(pullId);
if (!pull || pull.expiresAt < Date.now()) {
return new Response("Not found", { status: 404 });
}
const requesterClientId = req.headers.get("x-overlord-client-id") || "";
if (!requesterClientId || requesterClientId !== pull.clientId) {
return new Response("Forbidden", { status: 403 });
}
deletePluginPull(pullId);
logger.debug("[plugin] http pull", {
pullId,
clientId: pull.clientId,
pluginId: pull.pluginId,
bytes: pull.size,
});
const headers = {
...deps.secureHeaders("application/octet-stream"),
"Cache-Control": "no-store, private",
"Content-Length": String(pull.size),
};
return new Response(Bun.file(pull.binaryPath).stream(), { headers });
}
async function serveLoginOrUnauthorized(): Promise<Response> {
const loginFile = Bun.file(`${deps.PUBLIC_ROOT}/login.html`);
if (await loginFile.exists()) {