reg key editor plugin

This commit is contained in:
KingKDot
2026-06-17 16:15:36 -05:00
parent d5f210537e
commit da8bd8c961
5 changed files with 1768 additions and 0 deletions
+82
View File
@@ -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%
+763
View File
@@ -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 <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
#ifdef _WIN32
# define EXPORT extern "C" __declspec(dllexport)
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <winreg.h>
# 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<std::mutex> lk(g_mu);
if (!g_callback) return;
#ifdef _WIN32
g_callback(event, static_cast<uintptr_t>(strlen(event)),
payload.c_str(), static_cast<uintptr_t>(payload.size()));
#else
g_callback(g_callback_ctx, event, static_cast<int>(strlen(event)),
payload.c_str(), static_cast<int>(payload.size()));
#endif
}
static std::string trim(const std::string &s) {
size_t a = 0;
while (a < s.size() && std::isspace(static_cast<unsigned char>(s[a]))) a++;
size_t b = s.size();
while (b > a && std::isspace(static_cast<unsigned char>(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<char>(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<size_t>(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<unsigned char>(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<int>(s.size()), nullptr, 0);
std::wstring w(static_cast<size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(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<int>(w.size()), nullptr, 0, nullptr, nullptr);
std::string s(static_cast<size_t>(n), '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(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<char>(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<DWORD>(code),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPWSTR>(&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<long>(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<BYTE> &out) {
std::string h;
for (unsigned char c : text) {
if (std::isxdigit(c)) h += static_cast<char>(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<BYTE>(v & 0xff));
}
return true;
}
static std::string value_to_text(DWORD type, const std::vector<BYTE> &data) {
if (type == REG_DWORD && data.size() >= 4) {
DWORD v = *reinterpret_cast<const DWORD *>(data.data());
return std::to_string(v);
}
if (type == REG_QWORD && data.size() >= 8) {
unsigned long long v = *reinterpret_cast<const unsigned long long *>(data.data());
return std::to_string(v);
}
if (type == REG_SZ || type == REG_EXPAND_SZ) {
const wchar_t *w = reinterpret_cast<const wchar_t *>(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<const wchar_t *>(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<DWORD>(data.size()));
}
static bool text_to_value(DWORD type, const std::string &text, std::vector<BYTE> &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<BYTE> &data) {
std::string left = name.empty() ? "@" : reg_quote(name);
if (type == REG_DWORD && data.size() >= 4) {
DWORD v = *reinterpret_cast<const DWORD *>(data.data());
char buf[32];
std::snprintf(buf, sizeof(buf), "dword:%08lx", static_cast<unsigned long>(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<DWORD>(data.size())) + "\r\n";
if (type == REG_MULTI_SZ) return left + "=hex(7):" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
if (type == REG_QWORD) return left + "=hex(b):" + reg_hex_list(data.data(), static_cast<DWORD>(data.size())) + "\r\n";
return left + "=hex:" + reg_hex_list(data.data(), static_cast<DWORD>(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<DWORD>(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<DWORD>(name.size());
DWORD type = 0;
DWORD data_len = max_data + 2;
std::vector<BYTE> 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<DWORD>(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<BYTE> 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<DWORD>(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<DWORD>(name.size());
DWORD type = 0;
DWORD data_len = max_data + 2;
std::vector<BYTE> 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<std::string> subs;
for (DWORD i = 0; i < sub_count; i++) {
std::wstring name(max_sub + 2, L'\0');
DWORD len = static_cast<DWORD>(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<BYTE> 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<DWORD>(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<DWORD>(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, &current, 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<std::mutex> lk(g_mu);
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
}
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uint64_t cb) {
(void)hostInfo;
(void)hostInfoLen;
{
std::lock_guard<std::mutex> lk(g_mu);
g_callback = reinterpret_cast<host_callback_t>(static_cast<uintptr_t>(cb));
}
#else
EXPORT int PluginOnLoad(const char *hostInfo, int hostInfoLen, uintptr_t cb, uintptr_t ctx) {
(void)hostInfo;
(void)hostInfoLen;
{
std::lock_guard<std::mutex> lk(g_mu);
g_callback = reinterpret_cast<host_callback_t>(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<size_t>(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<std::mutex> lk(g_mu);
g_callback = nullptr;
#ifndef _WIN32
g_callback_ctx = 0;
#endif
}
+422
View File
@@ -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);
}
}
+109
View File
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Registry Editor</title>
<link rel="stylesheet" href="/plugins/regedit/assets/regedit.css" />
</head>
<body>
<main class="regedit-shell">
<header class="regedit-topbar">
<div>
<h1>Registry Editor</h1>
<p>Browse, edit, import, and export Windows registry keys on the selected client.</p>
</div>
<label class="client-box">
<span>Client ID</span>
<input id="client-id" placeholder="clientId" autocomplete="off" />
</label>
</header>
<section class="toolbar">
<button id="btn-refresh" class="btn primary"><i class="fa-solid fa-rotate"></i><span>Refresh</span></button>
<button id="btn-up" class="btn"><i class="fa-solid fa-arrow-up"></i><span>Up</span></button>
<button id="btn-new-key" class="btn"><i class="fa-solid fa-folder-plus"></i><span>New Key</span></button>
<button id="btn-new-value" class="btn"><i class="fa-solid fa-plus"></i><span>New Value</span></button>
<button id="btn-delete-key" class="btn danger"><i class="fa-solid fa-trash"></i><span>Delete Key</span></button>
<button id="btn-import" class="btn"><i class="fa-solid fa-file-import"></i><span>Import .reg</span></button>
<button id="btn-export" class="btn"><i class="fa-solid fa-file-export"></i><span>Export .reg</span></button>
</section>
<section class="pathbar">
<input id="path-input" spellcheck="false" value="HKCU" />
<button id="btn-go" class="btn primary"><i class="fa-solid fa-location-arrow"></i><span>Go</span></button>
</section>
<section class="workspace">
<aside class="tree-pane">
<div class="pane-title">Keys</div>
<div id="key-tree" class="key-tree"></div>
</aside>
<section class="value-pane">
<div class="pane-title">
<span id="current-key">HKCU</span>
<span id="status-pill" class="status-pill">Idle</span>
</div>
<table class="value-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Data</th>
<th></th>
</tr>
</thead>
<tbody id="value-body"></tbody>
</table>
</section>
</section>
<section class="console-pane">
<div class="pane-title">Activity</div>
<pre id="log"></pre>
</section>
</main>
<dialog id="value-dialog">
<form method="dialog" class="dialog-body">
<h2 id="value-dialog-title">Edit Value</h2>
<label>
<span>Name</span>
<input id="value-name" spellcheck="false" />
</label>
<label>
<span>Type</span>
<select id="value-type">
<option>REG_SZ</option>
<option>REG_EXPAND_SZ</option>
<option>REG_DWORD</option>
<option>REG_QWORD</option>
<option>REG_BINARY</option>
<option>REG_MULTI_SZ</option>
</select>
</label>
<label>
<span>Data</span>
<textarea id="value-data" rows="7" spellcheck="false"></textarea>
</label>
<div class="dialog-actions">
<button value="cancel" class="btn">Cancel</button>
<button id="btn-save-value" value="default" class="btn primary">Save</button>
</div>
</form>
</dialog>
<dialog id="reg-dialog">
<form method="dialog" class="dialog-body wide">
<h2 id="reg-dialog-title">.reg Data</h2>
<textarea id="reg-text" rows="18" spellcheck="false"></textarea>
<div class="dialog-actions">
<button value="cancel" class="btn">Close</button>
<button id="btn-run-import" value="default" class="btn primary">Import</button>
</div>
</form>
</dialog>
<script src="/plugins/regedit/assets/regedit.js"></script>
</body>
</html>
+392
View File
@@ -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 = `<span class="key-icon ${iconType}" aria-hidden="true"></span><span class="name"></span>`;
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 = `<td colspan="4" class="value-data">No values in this key.</td>`;
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 = `
<td><span class="value-name"><span class="value-icon"></span><span class="value-label"></span></span></td>
<td></td>
<td><div class="value-data"></div></td>
<td><div class="row-actions"></div></td>
`;
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 = `<i class="fa-solid fa-pen"></i>`;
edit.title = "Edit value";
edit.addEventListener("click", () => openValueDialog(value));
const del = document.createElement("button");
del.className = "btn danger";
del.innerHTML = `<i class="fa-solid fa-trash"></i>`;
del.title = "Delete value";
del.addEventListener("click", () => deleteValue(value.name || ""));
actions.append(edit, del);
valueBody.appendChild(tr);
}
}
function renderLoading(path) {
keyTree.innerHTML = `
<div class="loading-panel">
<span class="loading-spinner" aria-hidden="true"></span>
<span>Opening ${escapeHtml(path)}</span>
</div>
`;
valueBody.innerHTML = `
<tr>
<td colspan="4">
<div class="loading-panel table-loading">
<span class="loading-spinner" aria-hidden="true"></span>
<span>Enumerating values and subkeys...</span>
</div>
</td>
</tr>
`;
}
function escapeHtml(text) {
return String(text).replace(/[&<>"']/g, (ch) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
}[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(() => {});
})();