From da8bd8c9614c7e764e81ad748064eeaca68e44f4 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Wed, 17 Jun 2026 16:15:36 -0500 Subject: [PATCH] reg key editor plugin --- plugins/regedit/build.bat | 82 ++++ plugins/regedit/native/plugin.cpp | 763 ++++++++++++++++++++++++++++++ plugins/regedit/regedit.css | 422 +++++++++++++++++ plugins/regedit/regedit.html | 109 +++++ plugins/regedit/regedit.js | 392 +++++++++++++++ 5 files changed, 1768 insertions(+) create mode 100644 plugins/regedit/build.bat create mode 100644 plugins/regedit/native/plugin.cpp create mode 100644 plugins/regedit/regedit.css create mode 100644 plugins/regedit/regedit.html create mode 100644 plugins/regedit/regedit.js diff --git a/plugins/regedit/build.bat b/plugins/regedit/build.bat new file mode 100644 index 0000000..a2b1ee4 --- /dev/null +++ b/plugins/regedit/build.bat @@ -0,0 +1,82 @@ +@echo off +setlocal enabledelayedexpansion + +set "PLUGIN_DIR=%~dp0" +set "NATIVE_DIR=%PLUGIN_DIR%native" +set "PLUGIN_NAME=regedit" +set "ZIP_OUT=%PLUGIN_DIR%%PLUGIN_NAME%.zip" + +if not exist "%NATIVE_DIR%\plugin.cpp" ( + echo [error] native\plugin.cpp not found in %NATIVE_DIR% + exit /b 1 +) + +if not defined BUILD_TARGETS set "BUILD_TARGETS=windows-amd64" + +for %%T in (%BUILD_TARGETS%) do ( + for /f "tokens=1,2 delims=-" %%A in ("%%T") do ( + set "TARGET_OS=%%A" + set "TARGET_ARCH=%%B" + ) + + if not "!TARGET_OS!"=="windows" ( + echo [error] regedit plugin only supports windows targets, got !TARGET_OS!-!TARGET_ARCH! + exit /b 1 + ) + + set "OUTFILE=%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll" + if exist "!OUTFILE!" del /f /q "!OUTFILE!" + + set "CL_MACHINE=/machine:X64" + set "GXX_CMD=x86_64-w64-mingw32-g++" + if "!TARGET_ARCH!"=="arm64" ( + set "CL_MACHINE=/machine:ARM64" + set "GXX_CMD=aarch64-w64-mingw32-g++" + ) + + echo [build] cl /LD /EHsc /O2 plugin.cpp /Fe:!OUTFILE! + pushd "%NATIVE_DIR%" + cl /nologo /LD /EHsc /O2 plugin.cpp /Fe:"!OUTFILE!" advapi32.lib /link !CL_MACHINE! >nul 2>&1 + set "CL_STATUS=!errorlevel!" + popd + if not "!CL_STATUS!"=="0" ( + echo [build] cl failed, trying !GXX_CMD!... + !GXX_CMD! -shared -O2 -s -static -o "!OUTFILE!" "%NATIVE_DIR%\plugin.cpp" -ladvapi32 + if errorlevel 1 ( + echo [error] build failed for !TARGET_OS!-!TARGET_ARCH! + exit /b 1 + ) + ) + + if exist "%NATIVE_DIR%\plugin.obj" del /f /q "%NATIVE_DIR%\plugin.obj" + if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.exp" + if exist "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib" del /f /q "%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.lib" + + echo [ok] !OUTFILE! +) + +if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%" + +set "ZIP_SOURCES=" +for %%T in (%BUILD_TARGETS%) do ( + for /f "tokens=1,2 delims=-" %%A in ("%%T") do ( + set "TARGET_OS=%%A" + set "TARGET_ARCH=%%B" + ) + set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%-!TARGET_OS!-!TARGET_ARCH!.dll'" +) + +if exist "%PLUGIN_DIR%%PLUGIN_NAME%.html" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.html'" +if exist "%PLUGIN_DIR%%PLUGIN_NAME%.css" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.css'" +if exist "%PLUGIN_DIR%%PLUGIN_NAME%.js" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%%PLUGIN_NAME%.js'" +if exist "%PLUGIN_DIR%config.json" set "ZIP_SOURCES=!ZIP_SOURCES!,'%PLUGIN_DIR%config.json'" + +set "ZIP_SOURCES=!ZIP_SOURCES:~1!" + +powershell -NoProfile -Command "Compress-Archive -Path !ZIP_SOURCES! -DestinationPath '%ZIP_OUT%'" +if errorlevel 1 ( + echo [error] zip creation failed + exit /b 1 +) + +echo [ok] %ZIP_OUT% diff --git a/plugins/regedit/native/plugin.cpp b/plugins/regedit/native/plugin.cpp new file mode 100644 index 0000000..3fbf91b --- /dev/null +++ b/plugins/regedit/native/plugin.cpp @@ -0,0 +1,763 @@ +/* + * Registry Editor Plugin — Windows registry browser/editor. + * + * Exports the standard Overlord plugin ABI: + * PluginOnLoad, PluginOnEvent, PluginOnUnload, PluginSetCallback, + * PluginGetRuntime (returns "cpp") + * + * Build (Windows / MSVC): + * cl /LD /EHsc /O2 plugin.cpp /Fe:regedit-windows-amd64.dll advapi32.lib + * + * Build (Windows / MinGW): + * x86_64-w64-mingw32-g++ -shared -O2 -o regedit-windows-amd64.dll plugin.cpp -ladvapi32 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# define EXPORT extern "C" __declspec(dllexport) +# define WIN32_LEAN_AND_MEAN +# include +# include +# pragma comment(lib, "advapi32.lib") +BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) { + (void)hDll; + (void)reason; + (void)reserved; + return TRUE; +} +#else +# define EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +#ifdef _WIN32 +typedef void (__stdcall *host_callback_t)( + const char *event, uintptr_t eventLen, + const char *payload, uintptr_t payloadLen); +#else +typedef void (*host_callback_t)( + uintptr_t ctx, + const char *event, int eventLen, + const char *payload, int payloadLen); +#endif + +static std::mutex g_mu; +static host_callback_t g_callback = nullptr; +#ifndef _WIN32 +static uintptr_t g_callback_ctx = 0; +#endif + +static void send_event(const char *event, const std::string &payload) { + std::lock_guard lk(g_mu); + if (!g_callback) return; +#ifdef _WIN32 + g_callback(event, static_cast(strlen(event)), + payload.c_str(), static_cast(payload.size())); +#else + g_callback(g_callback_ctx, event, static_cast(strlen(event)), + payload.c_str(), static_cast(payload.size())); +#endif +} + +static std::string trim(const std::string &s) { + size_t a = 0; + while (a < s.size() && std::isspace(static_cast(s[a]))) a++; + size_t b = s.size(); + while (b > a && std::isspace(static_cast(s[b - 1]))) b--; + return s.substr(a, b - a); +} + +static std::string json_escape(const std::string &s) { + std::string out; + out.reserve(s.size() + 16); + for (unsigned char c : s) { + switch (c) { + case '\\': out += "\\\\"; break; + case '"': out += "\\\""; break; + case '\b': out += "\\b"; break; + case '\f': out += "\\f"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: + if (c < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04x", c); + out += buf; + } else { + out += static_cast(c); + } + } + } + return out; +} + +static std::string ok_payload(const std::string &action, const std::string &extra = "{}") { + std::string body = extra; + if (body.empty()) body = "{}"; + if (body.front() == '{') body.erase(body.begin()); + if (!body.empty() && body.back() == '}') body.pop_back(); + std::string out = "{\"ok\":true,\"action\":\"" + json_escape(action) + "\""; + if (!body.empty()) out += "," + body; + out += "}"; + return out; +} + +static std::string err_payload(const std::string &action, const std::string &error) { + return "{\"ok\":false,\"action\":\"" + json_escape(action) + + "\",\"error\":\"" + json_escape(error) + "\"}"; +} + +static std::string json_string(const char *json, int len, const char *key) { + if (!json || len <= 0) return ""; + std::string s(json, static_cast(len)); + std::string needle = std::string("\"") + key + "\""; + size_t p = s.find(needle); + if (p == std::string::npos) return ""; + p = s.find(':', p + needle.size()); + if (p == std::string::npos) return ""; + p++; + while (p < s.size() && std::isspace(static_cast(s[p]))) p++; + if (p >= s.size() || s[p] != '"') return ""; + p++; + std::string out; + while (p < s.size()) { + char c = s[p++]; + if (c == '"') break; + if (c == '\\' && p < s.size()) { + char e = s[p++]; + switch (e) { + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case '\\': out += '\\'; break; + case '"': out += '"'; break; + default: out += e; break; + } + } else { + out += c; + } + } + return out; +} + +#ifdef _WIN32 +static std::wstring widen(const std::string &s) { + if (s.empty()) return L""; + int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); + std::wstring w(static_cast(n), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), &w[0], n); + return w; +} + +static std::string narrow(const std::wstring &w) { + if (w.empty()) return ""; + int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); + std::string s(static_cast(n), '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast(w.size()), &s[0], n, nullptr, nullptr); + return s; +} + +struct RegPath { + HKEY root; + std::wstring subkey; + std::string root_name; +}; + +static bool parse_path(const std::string &path_in, RegPath &out, std::string &error) { + std::string path = trim(path_in); + std::replace(path.begin(), path.end(), '/', '\\'); + while (!path.empty() && path.back() == '\\') path.pop_back(); + size_t slash = path.find('\\'); + std::string root = slash == std::string::npos ? path : path.substr(0, slash); + std::string sub = slash == std::string::npos ? "" : path.substr(slash + 1); + std::string up = root; + std::transform(up.begin(), up.end(), up.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); + + if (up == "HKCR" || up == "HKEY_CLASSES_ROOT") { + out.root = HKEY_CLASSES_ROOT; out.root_name = "HKCR"; + } else if (up == "HKCU" || up == "HKEY_CURRENT_USER") { + out.root = HKEY_CURRENT_USER; out.root_name = "HKCU"; + } else if (up == "HKLM" || up == "HKEY_LOCAL_MACHINE") { + out.root = HKEY_LOCAL_MACHINE; out.root_name = "HKLM"; + } else if (up == "HKU" || up == "HKEY_USERS") { + out.root = HKEY_USERS; out.root_name = "HKU"; + } else if (up == "HKCC" || up == "HKEY_CURRENT_CONFIG") { + out.root = HKEY_CURRENT_CONFIG; out.root_name = "HKCC"; + } else { + error = "Unknown root hive: " + root; + return false; + } + out.subkey = widen(sub); + return true; +} + +static std::string full_path(const RegPath &p) { + std::string s = p.root_name; + if (!p.subkey.empty()) s += "\\" + narrow(p.subkey); + return s; +} + +static std::string win_error(LSTATUS code) { + wchar_t *msg = nullptr; + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + DWORD len = FormatMessageW(flags, nullptr, static_cast(code), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&msg), 0, nullptr); + std::string text; + if (len && msg) { + text = narrow(std::wstring(msg, msg + len)); + LocalFree(msg); + while (!text.empty() && (text.back() == '\r' || text.back() == '\n' || text.back() == ' ')) { + text.pop_back(); + } + } + char buf[64]; + std::snprintf(buf, sizeof(buf), "Windows error %ld", static_cast(code)); + return text.empty() ? std::string(buf) : text + " (" + buf + ")"; +} + +static const char *type_name(DWORD type) { + switch (type) { + case REG_SZ: return "REG_SZ"; + case REG_EXPAND_SZ: return "REG_EXPAND_SZ"; + case REG_DWORD: return "REG_DWORD"; + case REG_QWORD: return "REG_QWORD"; + case REG_BINARY: return "REG_BINARY"; + case REG_MULTI_SZ: return "REG_MULTI_SZ"; + default: return "REG_NONE"; + } +} + +static DWORD type_from_name(const std::string &name) { + if (name == "REG_SZ") return REG_SZ; + if (name == "REG_EXPAND_SZ") return REG_EXPAND_SZ; + if (name == "REG_DWORD") return REG_DWORD; + if (name == "REG_QWORD") return REG_QWORD; + if (name == "REG_BINARY") return REG_BINARY; + if (name == "REG_MULTI_SZ") return REG_MULTI_SZ; + return REG_SZ; +} + +static std::string hex_bytes(const BYTE *data, DWORD bytes) { + static const char *digits = "0123456789abcdef"; + std::string out; + for (DWORD i = 0; i < bytes; i++) { + if (i) out += ' '; + out += digits[(data[i] >> 4) & 0xf]; + out += digits[data[i] & 0xf]; + } + return out; +} + +static bool parse_hex_bytes(const std::string &text, std::vector &out) { + std::string h; + for (unsigned char c : text) { + if (std::isxdigit(c)) h += static_cast(c); + } + if (h.size() % 2 != 0) return false; + out.clear(); + for (size_t i = 0; i < h.size(); i += 2) { + unsigned int v = 0; + std::stringstream ss; + ss << std::hex << h.substr(i, 2); + ss >> v; + out.push_back(static_cast(v & 0xff)); + } + return true; +} + +static std::string value_to_text(DWORD type, const std::vector &data) { + if (type == REG_DWORD && data.size() >= 4) { + DWORD v = *reinterpret_cast(data.data()); + return std::to_string(v); + } + if (type == REG_QWORD && data.size() >= 8) { + unsigned long long v = *reinterpret_cast(data.data()); + return std::to_string(v); + } + if (type == REG_SZ || type == REG_EXPAND_SZ) { + const wchar_t *w = reinterpret_cast(data.data()); + size_t chars = data.size() / sizeof(wchar_t); + if (chars && w[chars - 1] == L'\0') chars--; + return narrow(std::wstring(w, w + chars)); + } + if (type == REG_MULTI_SZ) { + const wchar_t *w = reinterpret_cast(data.data()); + size_t chars = data.size() / sizeof(wchar_t); + std::string out; + size_t start = 0; + for (size_t i = 0; i < chars; i++) { + if (w[i] == L'\0') { + if (i == start) break; + if (!out.empty()) out += "\n"; + out += narrow(std::wstring(w + start, w + i)); + start = i + 1; + } + } + return out; + } + return hex_bytes(data.data(), static_cast(data.size())); +} + +static bool text_to_value(DWORD type, const std::string &text, std::vector &out, std::string &error) { + out.clear(); + if (type == REG_DWORD) { + char *end = nullptr; + unsigned long v = std::strtoul(text.c_str(), &end, 0); + if (end == text.c_str()) { error = "Invalid DWORD"; return false; } + out.resize(4); + std::memcpy(out.data(), &v, 4); + return true; + } + if (type == REG_QWORD) { + char *end = nullptr; + unsigned long long v = std::strtoull(text.c_str(), &end, 0); + if (end == text.c_str()) { error = "Invalid QWORD"; return false; } + out.resize(8); + std::memcpy(out.data(), &v, 8); + return true; + } + if (type == REG_BINARY) { + if (!parse_hex_bytes(text, out)) { error = "Invalid binary hex"; return false; } + return true; + } + if (type == REG_MULTI_SZ) { + std::wstring all; + std::stringstream ss(text); + std::string line; + while (std::getline(ss, line)) { + std::wstring w = widen(line); + all.append(w); + all.push_back(L'\0'); + } + all.push_back(L'\0'); + out.resize(all.size() * sizeof(wchar_t)); + std::memcpy(out.data(), all.data(), out.size()); + return true; + } + std::wstring w = widen(text); + w.push_back(L'\0'); + out.resize(w.size() * sizeof(wchar_t)); + std::memcpy(out.data(), w.data(), out.size()); + return true; +} + +static std::string reg_quote(const std::string &s) { + std::string out = "\""; + for (char c : s) { + if (c == '\\' || c == '"') out += '\\'; + out += c; + } + out += "\""; + return out; +} + +static std::string reg_hex_list(const BYTE *data, DWORD size) { + static const char *digits = "0123456789abcdef"; + std::string out; + for (DWORD i = 0; i < size; i++) { + if (i) out += ","; + out += digits[(data[i] >> 4) & 0xf]; + out += digits[data[i] & 0xf]; + } + return out; +} + +static std::string value_to_reg_line(const std::string &name, DWORD type, const std::vector &data) { + std::string left = name.empty() ? "@" : reg_quote(name); + if (type == REG_DWORD && data.size() >= 4) { + DWORD v = *reinterpret_cast(data.data()); + char buf[32]; + std::snprintf(buf, sizeof(buf), "dword:%08lx", static_cast(v)); + return left + "=" + buf + "\r\n"; + } + if (type == REG_SZ) return left + "=" + reg_quote(value_to_text(type, data)) + "\r\n"; + if (type == REG_EXPAND_SZ) return left + "=hex(2):" + reg_hex_list(data.data(), static_cast(data.size())) + "\r\n"; + if (type == REG_MULTI_SZ) return left + "=hex(7):" + reg_hex_list(data.data(), static_cast(data.size())) + "\r\n"; + if (type == REG_QWORD) return left + "=hex(b):" + reg_hex_list(data.data(), static_cast(data.size())) + "\r\n"; + return left + "=hex:" + reg_hex_list(data.data(), static_cast(data.size())) + "\r\n"; +} + +static bool open_key(const std::string &path, REGSAM access, HKEY &key, RegPath *parsed, std::string &error) { + RegPath rp; + if (!parse_path(path, rp, error)) return false; + LSTATUS st = RegOpenKeyExW(rp.root, rp.subkey.empty() ? nullptr : rp.subkey.c_str(), 0, access, &key); + if (st != ERROR_SUCCESS) { + error = win_error(st); + return false; + } + if (parsed) *parsed = rp; + return true; +} + +static void handle_list_key(const std::string &path) { + HKEY key = nullptr; + RegPath rp; + std::string error; + REGSAM list_access = KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS; + if (!open_key(path, list_access, key, &rp, error)) { + send_event("registry_result", + err_payload("list_key", + "Cannot open " + path + ": " + error)); + return; + } + + DWORD sub_count = 0, max_sub = 0, val_count = 0, max_val = 0, max_data = 0; + RegQueryInfoKeyW(key, nullptr, nullptr, nullptr, &sub_count, &max_sub, nullptr, + &val_count, &max_val, &max_data, nullptr, nullptr); + + std::ostringstream subkeys; + subkeys << "["; + for (DWORD i = 0; i < sub_count; i++) { + std::wstring name(max_sub + 2, L'\0'); + DWORD len = static_cast(name.size()); + if (RegEnumKeyExW(key, i, &name[0], &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { + if (i) subkeys << ","; + subkeys << "\"" << json_escape(narrow(std::wstring(name.c_str(), len))) << "\""; + } + } + subkeys << "]"; + + std::ostringstream values; + values << "["; + bool first = true; + for (DWORD i = 0; i < val_count; i++) { + std::wstring name(max_val + 2, L'\0'); + DWORD name_len = static_cast(name.size()); + DWORD type = 0; + DWORD data_len = max_data + 2; + std::vector data(data_len ? data_len : 2); + LSTATUS st = RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len); + if (st == ERROR_MORE_DATA) { + data.resize(data_len); + name_len = static_cast(name.size()); + st = RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len); + } + if (st == ERROR_SUCCESS) { + data.resize(data_len); + if (!first) values << ","; + first = false; + values << "{\"name\":\"" << json_escape(narrow(std::wstring(name.c_str(), name_len))) + << "\",\"type\":\"" << type_name(type) + << "\",\"data\":\"" << json_escape(value_to_text(type, data)) << "\"}"; + } + } + values << "]"; + RegCloseKey(key); + + send_event("registry_result", + ok_payload("list_key", "{\"path\":\"" + json_escape(full_path(rp)) + + "\",\"subkeys\":" + subkeys.str() + + ",\"values\":" + values.str() + "}")); +} + +static void handle_create_key(const std::string &path) { + RegPath rp; + std::string error; + if (!parse_path(path, rp, error)) { + send_event("registry_result", err_payload("create_key", error)); + return; + } + HKEY key = nullptr; + DWORD disp = 0; + LSTATUS st = RegCreateKeyExW(rp.root, rp.subkey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, &key, &disp); + if (st == ERROR_SUCCESS) { + RegCloseKey(key); + send_event("registry_result", ok_payload("create_key")); + } else { + send_event("registry_result", err_payload("create_key", win_error(st))); + } +} + +static void handle_delete_key(const std::string &path) { + RegPath rp; + std::string error; + if (!parse_path(path, rp, error)) { + send_event("registry_result", err_payload("delete_key", error)); + return; + } + if (rp.subkey.empty()) { + send_event("registry_result", err_payload("delete_key", "Root hives cannot be deleted")); + return; + } + LSTATUS st = RegDeleteTreeW(rp.root, rp.subkey.c_str()); + if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("delete_key")); + else send_event("registry_result", err_payload("delete_key", win_error(st))); +} + +static void handle_set_value(const char *payload, int payloadLen) { + std::string path = json_string(payload, payloadLen, "path"); + std::string name = json_string(payload, payloadLen, "name"); + std::string old_name = json_string(payload, payloadLen, "oldName"); + std::string type_s = json_string(payload, payloadLen, "type"); + std::string data_s = json_string(payload, payloadLen, "data"); + HKEY key = nullptr; + std::string error; + if (!open_key(path, KEY_SET_VALUE, key, nullptr, error)) { + send_event("registry_result", err_payload("set_value", error)); + return; + } + if (old_name != name && !old_name.empty()) { + RegDeleteValueW(key, widen(old_name).c_str()); + } + DWORD type = type_from_name(type_s); + std::vector data; + if (!text_to_value(type, data_s, data, error)) { + RegCloseKey(key); + send_event("registry_result", err_payload("set_value", error)); + return; + } + std::wstring wname = widen(name); + LSTATUS st = RegSetValueExW(key, name.empty() ? nullptr : wname.c_str(), 0, type, + data.empty() ? nullptr : data.data(), static_cast(data.size())); + RegCloseKey(key); + if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("set_value")); + else send_event("registry_result", err_payload("set_value", win_error(st))); +} + +static void handle_delete_value(const std::string &path, const std::string &name) { + HKEY key = nullptr; + std::string error; + if (!open_key(path, KEY_SET_VALUE, key, nullptr, error)) { + send_event("registry_result", err_payload("delete_value", error)); + return; + } + std::wstring wname = widen(name); + LSTATUS st = RegDeleteValueW(key, name.empty() ? nullptr : wname.c_str()); + RegCloseKey(key); + if (st == ERROR_SUCCESS) send_event("registry_result", ok_payload("delete_value")); + else send_event("registry_result", err_payload("delete_value", win_error(st))); +} + +static bool export_key_recursive(const std::string &path, std::string &out, std::string &error) { + HKEY key = nullptr; + RegPath rp; + if (!open_key(path, KEY_READ, key, &rp, error)) return false; + out += "\r\n[" + full_path(rp) + "]\r\n"; + + DWORD sub_count = 0, max_sub = 0, val_count = 0, max_val = 0, max_data = 0; + RegQueryInfoKeyW(key, nullptr, nullptr, nullptr, &sub_count, &max_sub, nullptr, + &val_count, &max_val, &max_data, nullptr, nullptr); + for (DWORD i = 0; i < val_count; i++) { + std::wstring name(max_val + 2, L'\0'); + DWORD name_len = static_cast(name.size()); + DWORD type = 0; + DWORD data_len = max_data + 2; + std::vector data(data_len ? data_len : 2); + if (RegEnumValueW(key, i, &name[0], &name_len, nullptr, &type, data.data(), &data_len) == ERROR_SUCCESS) { + data.resize(data_len); + out += value_to_reg_line(narrow(std::wstring(name.c_str(), name_len)), type, data); + } + } + std::vector subs; + for (DWORD i = 0; i < sub_count; i++) { + std::wstring name(max_sub + 2, L'\0'); + DWORD len = static_cast(name.size()); + if (RegEnumKeyExW(key, i, &name[0], &len, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { + subs.push_back(narrow(std::wstring(name.c_str(), len))); + } + } + RegCloseKey(key); + for (const auto &sub : subs) { + if (!export_key_recursive(path + "\\" + sub, out, error)) return false; + } + return true; +} + +static void handle_export_key(const std::string &path) { + std::string content = "Windows Registry Editor Version 5.00\r\n"; + std::string error; + if (!export_key_recursive(path, content, error)) { + send_event("registry_result", err_payload("export_key", error)); + return; + } + send_event("registry_result", + ok_payload("export_key", "{\"path\":\"" + json_escape(path) + + "\",\"content\":\"" + json_escape(content) + "\"}")); +} + +static std::string unquote_reg(const std::string &s) { + std::string t = trim(s); + if (t.size() >= 2 && t.front() == '"' && t.back() == '"') t = t.substr(1, t.size() - 2); + std::string out; + for (size_t i = 0; i < t.size(); i++) { + if (t[i] == '\\' && i + 1 < t.size()) out += t[++i]; + else out += t[i]; + } + return out; +} + +static bool import_value_line(HKEY key, const std::string &line, std::string &error) { + size_t eq = line.find('='); + if (eq == std::string::npos) return true; + std::string left = trim(line.substr(0, eq)); + std::string right = trim(line.substr(eq + 1)); + std::string name = left == "@" ? "" : unquote_reg(left); + std::wstring wname = widen(name); + + DWORD type = REG_SZ; + std::vector data; + if (right == "-") { + RegDeleteValueW(key, name.empty() ? nullptr : wname.c_str()); + return true; + } + if (right.rfind("dword:", 0) == 0) { + type = REG_DWORD; + DWORD v = static_cast(std::strtoul(right.substr(6).c_str(), nullptr, 16)); + data.resize(4); + std::memcpy(data.data(), &v, 4); + } else if (right.rfind("hex(b):", 0) == 0) { + type = REG_QWORD; + if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(b) value"; return false; } + } else if (right.rfind("hex(7):", 0) == 0) { + type = REG_MULTI_SZ; + if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(7) value"; return false; } + } else if (right.rfind("hex(2):", 0) == 0) { + type = REG_EXPAND_SZ; + if (!parse_hex_bytes(right.substr(7), data)) { error = "Invalid hex(2) value"; return false; } + } else if (right.rfind("hex:", 0) == 0) { + type = REG_BINARY; + if (!parse_hex_bytes(right.substr(4), data)) { error = "Invalid hex value"; return false; } + } else { + type = REG_SZ; + std::wstring w = widen(unquote_reg(right)); + w.push_back(L'\0'); + data.resize(w.size() * sizeof(wchar_t)); + std::memcpy(data.data(), w.data(), data.size()); + } + LSTATUS st = RegSetValueExW(key, name.empty() ? nullptr : wname.c_str(), 0, type, + data.empty() ? nullptr : data.data(), static_cast(data.size())); + if (st != ERROR_SUCCESS) { + error = win_error(st); + return false; + } + return true; +} + +static void handle_import_reg(const std::string &content) { + std::stringstream ss(content); + std::string raw, current_path, pending; + HKEY current = nullptr; + int changed = 0; + std::string error; + + auto close_current = [&]() { + if (current) RegCloseKey(current); + current = nullptr; + }; + + while (std::getline(ss, raw)) { + if (!raw.empty() && raw.back() == '\r') raw.pop_back(); + std::string line = trim(raw); + if (line.empty() || line[0] == ';') continue; + if (!line.empty() && line.back() == '\\') { + pending += line.substr(0, line.size() - 1); + continue; + } + if (!pending.empty()) { + line = pending + line; + pending.clear(); + } + if (line.rfind("Windows Registry Editor", 0) == 0 || line.rfind("REGEDIT4", 0) == 0) continue; + if (line.front() == '[' && line.back() == ']') { + close_current(); + current_path = line.substr(1, line.size() - 2); + if (!current_path.empty() && current_path[0] == '-') { + RegPath del; + if (parse_path(current_path.substr(1), del, error) && !del.subkey.empty()) { + RegDeleteTreeW(del.root, del.subkey.c_str()); + changed++; + } + current_path.clear(); + continue; + } + RegPath rp; + if (!parse_path(current_path, rp, error)) break; + LSTATUS st = RegCreateKeyExW(rp.root, rp.subkey.c_str(), 0, nullptr, 0, KEY_SET_VALUE, nullptr, ¤t, nullptr); + if (st != ERROR_SUCCESS) { error = win_error(st); break; } + changed++; + continue; + } + if (current) { + if (!import_value_line(current, line, error)) break; + changed++; + } + } + close_current(); + if (!error.empty()) send_event("registry_result", err_payload("import_reg", error)); + else send_event("registry_result", ok_payload("import_reg", "{\"changed\":" + std::to_string(changed) + "}")); +} +#endif + +EXPORT const char *PluginGetRuntime() { + return "cpp"; +} + +#ifdef _WIN32 +EXPORT void PluginSetCallback(uint64_t cb) { + std::lock_guard lk(g_mu); + g_callback = reinterpret_cast(static_cast(cb)); +} + +EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) { + (void)hostInfo; + (void)hostInfoLen; + { + std::lock_guard lk(g_mu); + g_callback = reinterpret_cast(static_cast(cb)); + } +#else +EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uintptr_t cb, uintptr_t ctx) { + (void)hostInfo; + (void)hostInfoLen; + { + std::lock_guard lk(g_mu); + g_callback = reinterpret_cast(cb); + g_callback_ctx = ctx; + } +#endif + send_event("ready", "{\"message\":\"registry editor plugin ready\"}"); + return 0; +} + +EXPORT int PluginOnEvent(const char *event, int eventLen, const char *payload, int payloadLen) { + std::string ev(event ? event : "", eventLen > 0 ? static_cast(eventLen) : 0u); +#ifndef _WIN32 + (void)payload; + (void)payloadLen; + send_event("registry_result", err_payload(ev, "Registry editor is Windows-only")); + return 0; +#else + if (ev == "list_key") handle_list_key(json_string(payload, payloadLen, "path")); + else if (ev == "create_key") handle_create_key(json_string(payload, payloadLen, "path")); + else if (ev == "delete_key") handle_delete_key(json_string(payload, payloadLen, "path")); + else if (ev == "set_value") handle_set_value(payload, payloadLen); + else if (ev == "delete_value") handle_delete_value(json_string(payload, payloadLen, "path"), + json_string(payload, payloadLen, "name")); + else if (ev == "export_key") handle_export_key(json_string(payload, payloadLen, "path")); + else if (ev == "import_reg") handle_import_reg(json_string(payload, payloadLen, "content")); + else send_event("registry_result", err_payload(ev, "Unknown event")); + return 0; +#endif +} + +EXPORT void PluginOnUnload() { + std::lock_guard lk(g_mu); + g_callback = nullptr; +#ifndef _WIN32 + g_callback_ctx = 0; +#endif +} diff --git a/plugins/regedit/regedit.css b/plugins/regedit/regedit.css new file mode 100644 index 0000000..762a294 --- /dev/null +++ b/plugins/regedit/regedit.css @@ -0,0 +1,422 @@ +:root { + color-scheme: dark; +} + +.regedit-shell { + min-height: calc(100vh - 92px); + display: grid; + grid-template-rows: auto auto auto minmax(420px, 1fr) 160px; + gap: 10px; + padding: 18px; + color: #e8edf4; + background: #111827; +} + +.regedit-topbar { + display: flex; + align-items: end; + justify-content: space-between; + gap: 18px; +} + +.regedit-topbar h1 { + margin: 0; + font-size: 26px; + line-height: 1.2; +} + +.regedit-topbar p { + margin: 4px 0 0; + color: #9aa7b7; +} + +.client-box { + display: grid; + gap: 4px; + min-width: min(340px, 100%); + color: #aeb8c6; + font-size: 12px; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid #334155; + border-radius: 6px; + background: #0b1220; + color: #f8fafc; + padding: 9px 10px; + font: inherit; +} + +textarea { + resize: vertical; + font-family: Consolas, "Courier New", monospace; +} + +.toolbar, +.pathbar { + display: flex; + align-items: center; + gap: 8px; +} + +.pathbar input { + flex: 1; + font-family: Consolas, "Courier New", monospace; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + min-height: 36px; + border: 1px solid #334155; + border-radius: 6px; + background: #1f2937; + color: #e5e7eb; + padding: 8px 11px; + cursor: pointer; +} + +.btn:hover { + background: #273449; +} + +.btn.primary { + border-color: #2563eb; + background: #1d4ed8; +} + +.btn.danger { + border-color: #7f1d1d; + background: #5f1f24; +} + +.workspace { + min-height: 0; + display: grid; + grid-template-columns: minmax(250px, 34%) minmax(360px, 1fr); + gap: 10px; +} + +.tree-pane, +.value-pane, +.console-pane { + min-height: 0; + border: 1px solid #243244; + border-radius: 8px; + background: #0f172a; + overflow: hidden; +} + +.pane-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + height: 38px; + border-bottom: 1px solid #243244; + padding: 0 12px; + color: #cbd5e1; + font-size: 13px; + font-weight: 700; +} + +#current-key { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-pill { + border: 1px solid #334155; + border-radius: 999px; + padding: 2px 8px; + color: #a7f3d0; + font-size: 12px; + font-weight: 600; +} + +.status-pill.loading { + color: #bfdbfe; +} + +.status-pill.loading::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + margin-right: 6px; + border: 2px solid #60a5fa; + border-top-color: transparent; + border-radius: 999px; + animation: regedit-spin 0.8s linear infinite; +} + +.key-tree { + height: calc(100% - 38px); + overflow: auto; + padding: 6px; +} + +.key-row { + display: flex; + align-items: center; + gap: 7px; + width: 100%; + border: 0; + border-radius: 5px; + background: transparent; + color: #dbe4ef; + padding: 7px 8px; + text-align: left; + cursor: pointer; +} + +.key-row:hover, +.key-row.active { + background: #1e293b; +} + +.regedit-browsing .key-row { + pointer-events: none; +} + +.key-row .name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.key-icon { + flex: 0 0 16px; + width: 16px; + height: 16px; + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +.key-icon.key, +.key-icon.hive { + background-image: url("data:image/x-icon;base64,AAABAAEAEBAQAAAAAACoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAALu7u7u7u7u7iIi7u7u7u7uIiIi7u7u7u4iIiIu7u7u7iIiIiIu7u7uIiIiIiIu7u4iIiIiIiIu7iIiIiIiIiLuIiIiIiIiIiLu7u7uIiIiIu7u7u7u7u7u7u7u7sAAAALu7u7sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAP8AAP//AAA="); +} + +.key-icon.root { + background-image: url("data:image/x-icon;base64,AAABAAEAEBAQAAAAAACoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AZmZmZmZmYABu7m7ubu5gAG7ubu5u7mAAbu5u7m7uYABmZmZmZmZgAG7uZmZmZmAAbu5mZmZmYABu7mZmZmZgAGZmZmZmZmAAbu5mZmAAYABu7mZmYAZmAG7uZmZgZuZgZmZmZmZu7mYAAAAAAGbmYAAAAAAABmYAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAAHAAAABwAAAAcAAAB3AAAAYwAAAEEAAAAAAAD/wQAA/+MAAP/3AAA="); +} + +.key-icon.up::before { + content: "\f062"; + display: block; + color: #cbd5e1; + font-family: "Font Awesome 6 Free"; + font-size: 13px; + font-weight: 900; + line-height: 16px; + text-align: center; +} + +.value-pane { + overflow: auto; +} + +.loading-panel { + display: flex; + align-items: center; + gap: 9px; + border: 1px solid #243244; + border-radius: 6px; + margin: 8px; + padding: 10px 11px; + color: #b8c8dc; + background: #111c2f; + font-size: 13px; +} + +.table-loading { + margin: 0; +} + +.loading-spinner { + flex: 0 0 16px; + width: 16px; + height: 16px; + border: 2px solid #60a5fa; + border-top-color: transparent; + border-radius: 999px; + animation: regedit-spin 0.8s linear infinite; +} + +@keyframes regedit-spin { + to { transform: rotate(360deg); } +} + +.value-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +.value-table th, +.value-table td { + border-bottom: 1px solid #1e293b; + padding: 9px 10px; + vertical-align: top; +} + +.value-table th { + position: sticky; + top: 0; + background: #101b2e; + color: #9fb0c4; + font-size: 12px; + text-align: left; +} + +.value-table th:nth-child(1) { width: 26%; } +.value-table th:nth-child(2) { width: 140px; } +.value-table th:nth-child(4) { width: 130px; } + +.value-name { + display: flex; + align-items: center; + gap: 7px; + min-width: 0; + font-family: Consolas, "Courier New", monospace; +} + +.value-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.value-icon { + position: relative; + flex: 0 0 16px; + width: 16px; + height: 16px; + border: 1px solid #8a9bb1; + border-radius: 2px; + background: linear-gradient(#f7fbff, #dfe8f4); + box-shadow: inset 0 0 0 1px #ffffffb0; + color: #164a7a; + font-family: Tahoma, Arial, sans-serif; + font-weight: 700; + line-height: 14px; + text-align: center; +} + +.value-icon.string::before { + content: "ab"; + font-size: 8px; +} + +.value-icon.number::before { + content: "01"; + font-size: 8px; +} + +.value-icon.binary::before { + content: "10"; + font-size: 8px; +} + +.value-icon.unknown::before { + content: "?"; + font-size: 10px; +} + +.value-data { + max-height: 96px; + overflow: auto; + color: #cbd5e1; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.row-actions { + display: flex; + gap: 6px; +} + +.row-actions .btn { + min-height: 30px; + padding: 5px 8px; +} + +.console-pane pre { + height: calc(100% - 38px); + margin: 0; + overflow: auto; + padding: 10px 12px; + color: #b6c2d1; + font-size: 12px; +} + +dialog { + border: 1px solid #334155; + border-radius: 8px; + background: #0f172a; + color: #e5e7eb; + padding: 0; +} + +dialog::backdrop { + background: rgb(0 0 0 / 58%); +} + +.dialog-body { + display: grid; + gap: 12px; + min-width: min(520px, 92vw); + padding: 18px; +} + +.dialog-body.wide { + min-width: min(760px, 94vw); +} + +.dialog-body h2 { + margin: 0 0 4px; + font-size: 18px; +} + +.dialog-body label { + display: grid; + gap: 5px; + color: #b8c3d1; + font-size: 13px; +} + +.dialog-actions { + display: flex; + justify-content: end; + gap: 8px; +} + +@media (max-width: 820px) { + .regedit-shell { + padding: 12px; + grid-template-rows: auto auto auto minmax(640px, 1fr) 150px; + } + + .regedit-topbar, + .toolbar, + .pathbar { + align-items: stretch; + flex-direction: column; + } + + .workspace { + grid-template-columns: 1fr; + grid-template-rows: 260px minmax(360px, 1fr); + } +} diff --git a/plugins/regedit/regedit.html b/plugins/regedit/regedit.html new file mode 100644 index 0000000..ac4ebba --- /dev/null +++ b/plugins/regedit/regedit.html @@ -0,0 +1,109 @@ + + + + + + Registry Editor + + + +
+
+
+

Registry Editor

+

Browse, edit, import, and export Windows registry keys on the selected client.

+
+ +
+ +
+ + + + + + + +
+ +
+ + +
+ +
+ +
+
+ HKCU + Idle +
+ + + + + + + + + + +
NameTypeData
+
+
+ +
+
Activity
+

+      
+
+ + +
+

Edit Value

+ + + +
+ + +
+
+
+ + +
+

.reg Data

+ +
+ + +
+
+
+ + + + diff --git a/plugins/regedit/regedit.js b/plugins/regedit/regedit.js new file mode 100644 index 0000000..5c41829 --- /dev/null +++ b/plugins/regedit/regedit.js @@ -0,0 +1,392 @@ +(() => { + const PLUGIN_ID = "regedit"; + const params = new URLSearchParams(window.location.search); + + const clientInput = document.getElementById("client-id"); + const pathInput = document.getElementById("path-input"); + const keyTree = document.getElementById("key-tree"); + const valueBody = document.getElementById("value-body"); + const currentKey = document.getElementById("current-key"); + const statusPill = document.getElementById("status-pill"); + const logEl = document.getElementById("log"); + const navButtons = [ + document.getElementById("btn-refresh"), + document.getElementById("btn-up"), + document.getElementById("btn-go"), + ].filter(Boolean); + + const valueDialog = document.getElementById("value-dialog"); + const valueTitle = document.getElementById("value-dialog-title"); + const valueName = document.getElementById("value-name"); + const valueType = document.getElementById("value-type"); + const valueData = document.getElementById("value-data"); + const saveValueBtn = document.getElementById("btn-save-value"); + + const regDialog = document.getElementById("reg-dialog"); + const regTitle = document.getElementById("reg-dialog-title"); + const regText = document.getElementById("reg-text"); + const runImportBtn = document.getElementById("btn-run-import"); + + const ROOT_PATH = "Computer"; + const HIVES = [ + ["HKEY_CLASSES_ROOT", "HKCR"], + ["HKEY_CURRENT_USER", "HKCU"], + ["HKEY_LOCAL_MACHINE", "HKLM"], + ["HKEY_USERS", "HKU"], + ["HKEY_CURRENT_CONFIG", "HKCC"], + ]; + + let activePath = ROOT_PATH; + let lastGoodPath = ROOT_PATH; + let pendingBrowsePath = null; + let isBrowsing = false; + let editingOriginalName = null; + let pollTimer = null; + + clientInput.value = params.get("clientId") || ""; + + function log(line) { + const ts = new Date().toLocaleTimeString(); + logEl.textContent = `${ts} ${line}\n` + logEl.textContent; + } + + function setStatus(text) { + statusPill.textContent = text; + statusPill.classList.toggle("loading", text === "Loading"); + } + + function setBrowsing(browsing) { + isBrowsing = browsing; + document.body.classList.toggle("regedit-browsing", browsing); + for (const btn of navButtons) btn.disabled = browsing; + } + + function getClientId() { + return clientInput.value.trim(); + } + + async function sendEvent(event, payload = {}) { + const clientId = getClientId(); + if (!clientId) { + log("Missing clientId"); + throw new Error("Missing clientId"); + } + setStatus("Working"); + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ event, payload }), + }); + if (!res.ok) { + const text = await res.text(); + setStatus("Error"); + log(`Send failed: ${res.status} ${text}`); + throw new Error(text); + } + log(`Sent ${event}`); + } + + async function pollEvents() { + const clientId = getClientId(); + if (!clientId) return; + try { + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/events`); + if (!res.ok) return; + const data = await res.json(); + for (const item of data.events || []) handlePluginEvent(item.event, item.payload); + } catch (_) {} + } + + function handlePluginEvent(event, payload) { + if (event === "ready") { + log(payload?.message || "Registry editor ready"); + return; + } + if (event !== "registry_result") return; + setStatus(payload?.ok ? "Ready" : "Error"); + if (!payload?.ok) { + const action = payload?.action || "operation"; + const error = payload?.error || "unknown error"; + log(`${action} failed: ${error}`); + if (action === "list_key") { + const failedPath = pendingBrowsePath || pathInput.value || activePath; + activePath = lastGoodPath; + pendingBrowsePath = null; + setBrowsing(false); + pathInput.value = activePath; + currentKey.textContent = activePath; + currentKey.title = activePath; + setStatus("Blocked"); + alert(`Cannot open ${failedPath}.\n\n${error}`); + } + return; + } + if (payload.action === "list_key") { + activePath = payload.path || activePath; + lastGoodPath = activePath; + pendingBrowsePath = null; + setBrowsing(false); + pathInput.value = activePath; + currentKey.textContent = activePath; + currentKey.title = activePath; + renderKeys(payload.subkeys || []); + renderValues(payload.values || []); + log(`Loaded ${activePath}`); + } else if (payload.action === "export_key") { + regTitle.textContent = `Export ${payload.path || activePath}`; + regText.value = payload.content || ""; + runImportBtn.style.display = "none"; + regDialog.showModal(); + log(`Exported ${payload.path || activePath}`); + } else { + log(`${payload.action || "operation"} completed`); + listKey(activePath); + } + } + + function renderKeys(keys) { + keyTree.innerHTML = ""; + if (activePath === ROOT_PATH) { + for (const [label, path] of HIVES) { + keyTree.appendChild(keyButton(label, path, "hive")); + } + return; + } + const up = parentPath(activePath); + if (up) { + const row = keyButton("..", up, "up"); + keyTree.appendChild(row); + } + for (const key of keys) { + keyTree.appendChild(keyButton(key, `${activePath}\\${key}`, "key")); + } + } + + function keyButton(label, path, iconType) { + const btn = document.createElement("button"); + btn.className = "key-row"; + btn.innerHTML = ``; + btn.querySelector(".name").textContent = label; + btn.title = path; + btn.addEventListener("click", () => listKey(path)); + return btn; + } + + function renderValues(values) { + valueBody.innerHTML = ""; + if (!values.length) { + const tr = document.createElement("tr"); + tr.innerHTML = `No values in this key.`; + valueBody.appendChild(tr); + return; + } + for (const value of values) { + const tr = document.createElement("tr"); + const displayName = value.name || "(Default)"; + const type = value.type || "REG_NONE"; + tr.innerHTML = ` + + +
+
+ `; + tr.children[0].title = displayName; + tr.querySelector(".value-label").textContent = displayName; + tr.querySelector(".value-icon").classList.add(valueIconClass(type)); + tr.children[1].textContent = type; + tr.querySelector(".value-data").textContent = value.data ?? ""; + tr.querySelector(".value-data").title = value.data ?? ""; + const actions = tr.querySelector(".row-actions"); + const edit = document.createElement("button"); + edit.className = "btn"; + edit.innerHTML = ``; + edit.title = "Edit value"; + edit.addEventListener("click", () => openValueDialog(value)); + const del = document.createElement("button"); + del.className = "btn danger"; + del.innerHTML = ``; + del.title = "Delete value"; + del.addEventListener("click", () => deleteValue(value.name || "")); + actions.append(edit, del); + valueBody.appendChild(tr); + } + } + + function renderLoading(path) { + keyTree.innerHTML = ` +
+ + Opening ${escapeHtml(path)} +
+ `; + valueBody.innerHTML = ` + + +
+ + Enumerating values and subkeys... +
+ + + `; + } + + function escapeHtml(text) { + return String(text).replace(/[&<>"']/g, (ch) => ({ + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }[ch])); + } + + function valueIconClass(type) { + if (type === "REG_SZ" || type === "REG_EXPAND_SZ" || type === "REG_MULTI_SZ") { + return "string"; + } + if (type === "REG_DWORD" || type === "REG_QWORD") return "number"; + if (type === "REG_BINARY") return "binary"; + return "unknown"; + } + + function parentPath(path) { + if (path === ROOT_PATH) return null; + const i = path.lastIndexOf("\\"); + return i > 0 ? path.slice(0, i) : ROOT_PATH; + } + + function listKey(path) { + if (isBrowsing) return Promise.resolve(); + const targetPath = normalizePath(path); + if (targetPath === ROOT_PATH) { + activePath = ROOT_PATH; + lastGoodPath = ROOT_PATH; + pendingBrowsePath = null; + pathInput.value = activePath; + currentKey.textContent = activePath; + currentKey.title = activePath; + setStatus("Ready"); + setBrowsing(false); + renderKeys([]); + renderValues([]); + log("Loaded Computer"); + return Promise.resolve(); + } + pendingBrowsePath = targetPath; + pathInput.value = targetPath; + currentKey.textContent = targetPath; + currentKey.title = targetPath; + setStatus("Loading"); + setBrowsing(true); + renderLoading(targetPath); + log(`Opening ${targetPath}`); + return sendEvent("list_key", { path: targetPath }); + } + + function normalizePath(path) { + const raw = (path || "").trim(); + if (!raw || raw.toLowerCase() === "computer" || raw === "\\") return ROOT_PATH; + return raw.replace(/^Computer\\/i, ""); + } + + function openValueDialog(value) { + editingOriginalName = value ? value.name || "" : null; + valueTitle.textContent = value ? "Edit Value" : "New Value"; + valueName.value = value ? value.name || "" : ""; + valueType.value = value ? value.type || "REG_SZ" : "REG_SZ"; + valueData.value = value ? value.data || "" : ""; + valueDialog.showModal(); + } + + function deleteValue(name) { + const display = name || "(Default)"; + if (!confirm(`Delete value ${display}?`)) return; + sendEvent("delete_value", { path: activePath, name }); + } + + document.getElementById("btn-refresh").addEventListener("click", () => listKey(pathInput.value)); + document.getElementById("btn-go").addEventListener("click", () => listKey(pathInput.value)); + document.getElementById("btn-up").addEventListener("click", () => { + const up = parentPath(activePath); + if (up) listKey(up); + }); + + document.getElementById("btn-new-key").addEventListener("click", () => { + if (activePath === ROOT_PATH) { + alert("Select a hive first."); + return; + } + const name = prompt("New key name"); + if (!name) return; + sendEvent("create_key", { path: `${activePath}\\${name}` }); + }); + + document.getElementById("btn-delete-key").addEventListener("click", () => { + if (activePath === ROOT_PATH || parentPath(activePath) === ROOT_PATH) { + alert("Root hives cannot be deleted."); + return; + } + if (!confirm(`Delete key and all subkeys?\n${activePath}`)) return; + const next = parentPath(activePath); + sendEvent("delete_key", { path: activePath, nextPath: next }); + activePath = next; + }); + + document.getElementById("btn-new-value").addEventListener("click", () => { + if (activePath === ROOT_PATH) { + alert("Select a registry key first."); + return; + } + openValueDialog(null); + }); + + saveValueBtn.addEventListener("click", (event) => { + event.preventDefault(); + const payload = { + path: activePath, + name: valueName.value, + oldName: editingOriginalName, + type: valueType.value, + data: valueData.value, + }; + valueDialog.close(); + sendEvent("set_value", payload); + }); + + document.getElementById("btn-export").addEventListener("click", () => { + if (activePath === ROOT_PATH) { + alert("Select a hive or key to export."); + return; + } + sendEvent("export_key", { path: activePath }); + }); + + document.getElementById("btn-import").addEventListener("click", () => { + regTitle.textContent = "Import .reg"; + regText.value = "Windows Registry Editor Version 5.00\n\n"; + runImportBtn.style.display = ""; + regDialog.showModal(); + }); + + runImportBtn.addEventListener("click", (event) => { + event.preventDefault(); + if (!confirm("Import this .reg data on the remote client?")) return; + const content = regText.value; + regDialog.close(); + sendEvent("import_reg", { content, refreshPath: activePath }); + }); + + pathInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") listKey(pathInput.value); + }); + + clientInput.addEventListener("change", () => { + if (pollTimer) clearInterval(pollTimer); + pollTimer = setInterval(pollEvents, 900); + listKey(pathInput.value || ROOT_PATH).catch(() => {}); + }); + + pollTimer = setInterval(pollEvents, 900); + listKey(activePath).catch(() => {}); +})();