Add a one-shot exec_command MCP tool that runs a command on a remote Windows host and returns stdout plus exit code, reusing the Web terminal link (main-connection COMMAND_SHELL, a shell sub-connection, and a sentinel command line located via rfind to tolerate ConPTY echo). The sentinel marker is embedded in the command line ConPTY echoes back, so it is only treated as hit when it starts a line (preceded by a newline or the buffer start); this keeps the echoed marker from being mistaken for the real sentinel when the echo packet arrives before the output. Execution is gated at Web remote-desktop sensitivity: a read-only mode (McpReadonly, default on, hides the tool) and a command whitelist (McpCmdWhitelist) with built-in read-only prefixes. Shell metacharacters (& | < > ^) are rejected before whitelist matching, and each execution is recorded in the server audit log. Extend the MCP settings dialog with the read-only checkbox and a multi-line whitelist box (commas and newlines both accepted, normalized to a comma-separated list on save), and add English and Traditional Chinese mappings for the new UI and audit-log strings. Co-Authored-By: deepseek-v4-pro
2582 lines
100 KiB
C++
2582 lines
100 KiB
C++
#include "stdafx.h"
|
||
#include "McpServer.h"
|
||
#include "jsoncpp/json.h"
|
||
#include "HostJson.h" // BuildHostJson(单台主机序列化公共函数)
|
||
#include "context.h" // context 接口
|
||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs/m_ClientMap)+ VERSION_STR
|
||
#include "LangManager.h" // _TR(审计日志标题语言映射)
|
||
|
||
#include <sstream>
|
||
|
||
#ifndef _WIN64
|
||
#ifdef _DEBUG
|
||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||
#else
|
||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||
#endif
|
||
#else
|
||
#ifdef _DEBUG
|
||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||
#else
|
||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||
#endif
|
||
#endif
|
||
|
||
namespace {
|
||
|
||
// P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。
|
||
static const int kMcpToolTimeoutMs = 20000;
|
||
|
||
// Json::Value → 紧凑 JSON 字符串
|
||
std::string JsonToString(const Json::Value& v) {
|
||
Json::StreamWriterBuilder b;
|
||
b["indentation"] = "";
|
||
return Json::writeString(b, v);
|
||
}
|
||
|
||
// JSON-RPC 2.0 成功响应
|
||
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
|
||
Json::Value resp(Json::objectValue);
|
||
resp["jsonrpc"] = "2.0";
|
||
resp["id"] = id;
|
||
resp["result"] = result;
|
||
return JsonToString(resp);
|
||
}
|
||
|
||
// JSON-RPC 2.0 错误响应
|
||
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
|
||
Json::Value resp(Json::objectValue);
|
||
resp["jsonrpc"] = "2.0";
|
||
resp["id"] = id;
|
||
Json::Value err(Json::objectValue);
|
||
err["code"] = code;
|
||
err["message"] = msg;
|
||
resp["error"] = err;
|
||
return JsonToString(resp);
|
||
}
|
||
|
||
// initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo)
|
||
std::string BuildInitializeResult(const Json::Value& id) {
|
||
Json::Value result(Json::objectValue);
|
||
result["protocolVersion"] = "2025-06-18";
|
||
Json::Value caps(Json::objectValue);
|
||
caps["tools"] = Json::Value(Json::objectValue);
|
||
result["capabilities"] = caps;
|
||
Json::Value serverInfo(Json::objectValue);
|
||
serverInfo["name"] = "yama";
|
||
serverInfo["version"] = VERSION_STR;
|
||
result["serverInfo"] = serverInfo;
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// ping 健康检查:返回空 result
|
||
std::string BuildPingResult(const Json::Value& id) {
|
||
return BuildResult(id, Json::Value(Json::objectValue));
|
||
}
|
||
|
||
// ========== P2a 通用辅助 ==========
|
||
|
||
// 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配
|
||
std::string ToLowerAscii(const std::string& s) {
|
||
std::string r = s;
|
||
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||
return r;
|
||
}
|
||
|
||
// 不区分大小写的子串匹配
|
||
bool ContainsCI(const std::string& haystack, const std::string& needle) {
|
||
if (needle.empty()) return true;
|
||
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
|
||
}
|
||
|
||
// 取对象的字符串字段,缺失/非字符串返回 ""
|
||
std::string JsonStrField(const Json::Value& v, const char* key) {
|
||
if (v.isObject() && v.isMember(key) && v[key].isString())
|
||
return v[key].asString();
|
||
return "";
|
||
}
|
||
|
||
// 是否纯数字(host id 为 uint64 十进制字符串)
|
||
bool IsDigits(const std::string& s) {
|
||
if (s.empty()) return false;
|
||
for (char c : s) if (c < '0' || c > '9') return false;
|
||
return true;
|
||
}
|
||
|
||
// 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象)
|
||
Json::Value GetCallArguments(const Json::Value& params) {
|
||
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
|
||
return params["arguments"];
|
||
return Json::Value(Json::objectValue);
|
||
}
|
||
|
||
// 读取可选字符串入参,缺失返回 ""
|
||
std::string GetStringArg(const Json::Value& args, const char* key) {
|
||
return JsonStrField(args, key);
|
||
}
|
||
|
||
// ========== P2b 辅助 ==========
|
||
|
||
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。
|
||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
|
||
std::string sid = GetStringArg(args, "id");
|
||
if (sid.empty()) {
|
||
err = "Missing required parameter: id";
|
||
return false;
|
||
}
|
||
if (!IsDigits(sid)) {
|
||
err = "Invalid id: expected a decimal host id string";
|
||
return false;
|
||
}
|
||
out = strtoull(sid.c_str(), nullptr, 10);
|
||
return true;
|
||
}
|
||
|
||
// 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。
|
||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
|
||
if (!parent) return nullptr;
|
||
context* found = nullptr;
|
||
EnterCriticalSection(&parent->m_cs);
|
||
for (context* ctx : parent->m_HostList) {
|
||
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
|
||
found = ctx;
|
||
break;
|
||
}
|
||
}
|
||
LeaveCriticalSection(&parent->m_cs);
|
||
return found;
|
||
}
|
||
|
||
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||
size_t BoundedStrlen(const char* p, size_t avail) {
|
||
size_t n = 0;
|
||
while (n < avail && p[n]) ++n;
|
||
return n;
|
||
}
|
||
|
||
// 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。
|
||
std::string ToUtf8(const char* s, UINT cp) {
|
||
if (!s || !*s) return "";
|
||
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||
if (wlen <= 0) return "";
|
||
std::wstring w(wlen - 1, L'\0');
|
||
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||
if (u8len <= 0) return "";
|
||
std::string out(u8len - 1, '\0');
|
||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
|
||
return out;
|
||
}
|
||
|
||
// 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的
|
||
// CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。
|
||
// 非 UTF-8(如纯 ASCII)失败时原样返回。
|
||
std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||
if (utf8.empty()) return "";
|
||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||
if (wlen <= 0) return utf8;
|
||
std::wstring w(wlen, L'\0');
|
||
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
|
||
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
|
||
if (alen <= 0) return utf8;
|
||
std::string out(alen, '\0');
|
||
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
|
||
return out;
|
||
}
|
||
|
||
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
||
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
||
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
||
title.clear();
|
||
status = "normal";
|
||
pid = 0;
|
||
if (!s || !*s) return;
|
||
std::string t(s);
|
||
int len = (int)t.size();
|
||
int pipePos[4] = { -1, -1, -1, -1 };
|
||
int cnt = 0;
|
||
for (int i = len - 1; i >= 0 && cnt < 4; --i) {
|
||
if (t[i] == '|') pipePos[cnt++] = i;
|
||
}
|
||
if (cnt < 4) { // 老格式:只有标题
|
||
title = t;
|
||
return;
|
||
}
|
||
pid = strtoull(t.c_str() + pipePos[2] + 1, nullptr, 10);
|
||
status = t.substr(pipePos[3] + 1, pipePos[2] - pipePos[3] - 1);
|
||
title = t.substr(0, pipePos[3]);
|
||
}
|
||
|
||
// 解析 TOKEN_PSLIST 缓冲(data[0]=token,其后为 [pid:4][name:arch\0][path\0] 记录)。
|
||
// 进程名/路径为客户端 ANSI(编码由 cp 指定),转 UTF-8 输出。
|
||
Json::Value ParseProcessList(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 2) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t len = data.size();
|
||
size_t off = 1; // 跳过 TOKEN 字节
|
||
while (off + sizeof(DWORD) <= len) {
|
||
DWORD pid = *(const DWORD*)(p + off);
|
||
off += sizeof(DWORD);
|
||
|
||
const char* exeFile = p + off;
|
||
size_t exeLen = BoundedStrlen(exeFile, len - off);
|
||
if (exeLen >= len - off) break; // 未以 '\0' 结尾,异常数据
|
||
if (exeLen == 0) break; // 空进程名 = 尾部零填充(LocalSize 对齐),停止解析
|
||
off += exeLen + 1;
|
||
|
||
const char* fullPath = p + off;
|
||
size_t pathLen = BoundedStrlen(fullPath, len - off);
|
||
if (pathLen >= len - off) break;
|
||
off += pathLen + 1;
|
||
|
||
std::string name(exeFile, exeLen);
|
||
std::string arch;
|
||
size_t colon = name.find(':');
|
||
if (colon != std::string::npos) {
|
||
arch = name.substr(colon + 1);
|
||
name = name.substr(0, colon);
|
||
}
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["pid"] = (Json::UInt64)pid;
|
||
item["name"] = ToUtf8(name.c_str(), cp);
|
||
item["arch"] = arch.empty() ? "N/A" : ToUtf8(arch.c_str(), cp);
|
||
item["path"] = ToUtf8(fullPath, cp);
|
||
arr.append(item);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 TOKEN_WSLIST 缓冲(data[0]=token,其后为 [hwnd:4][title|status|pid|r1|r2\0] 记录)。
|
||
// 窗口标题为客户端 UTF-8(老客户端为 CP_ACP),用 cp 解码后转 UTF-8 输出。
|
||
Json::Value ParseWindowList(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 2) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t len = data.size();
|
||
size_t off = 1;
|
||
while (off + sizeof(DWORD) <= len) {
|
||
DWORD hwnd = *(const DWORD*)(p + off);
|
||
off += sizeof(DWORD);
|
||
|
||
const char* titleWithAttrs = p + off;
|
||
size_t tlen = BoundedStrlen(titleWithAttrs, len - off);
|
||
if (tlen >= len - off) break;
|
||
if (tlen == 0) break; // 空记录 = 尾部零填充(LocalSize 对齐),停止解析
|
||
off += tlen + 1;
|
||
|
||
std::string title, status;
|
||
uint64_t pid = 0;
|
||
ParseWindowAttrsStr(titleWithAttrs, title, status, pid);
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["hwnd"] = (Json::UInt64)hwnd;
|
||
item["title"] = ToUtf8(title.c_str(), cp);
|
||
item["status"] = status;
|
||
item["pid"] = (Json::UInt64)pid;
|
||
arr.append(item);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 TOKEN_DRIVE_LIST 缓冲(data[0]=token,其后为
|
||
// [letter:1][GetDriveType:1][totalMB:4][freeMB:4][typeName\0][fileSystem\0] 记录,
|
||
// 以 letter=='\0' 终止)。typeName/fileSystem 为客户端 ANSI(编码由 cp 指定)。
|
||
Json::Value ParseDriveList(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 2) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t len = data.size();
|
||
size_t off = 1; // 跳过 TOKEN 字节
|
||
while (off + 10 <= len && p[off] != '\0') {
|
||
char letter = p[off];
|
||
unsigned int type = (unsigned int)(unsigned char)p[off + 1];
|
||
DWORD totalMB = *(const DWORD*)(p + off + 2);
|
||
DWORD freeMB = *(const DWORD*)(p + off + 6);
|
||
off += 10;
|
||
|
||
const char* typeName = p + off;
|
||
size_t tlen = BoundedStrlen(typeName, len - off);
|
||
if (tlen >= len - off) break;
|
||
off += tlen + 1;
|
||
|
||
const char* fileSystem = p + off;
|
||
size_t flen = BoundedStrlen(fileSystem, len - off);
|
||
if (flen >= len - off) break;
|
||
off += flen + 1;
|
||
|
||
std::string drive;
|
||
drive += letter;
|
||
drive += ":\\";
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["drive"] = drive;
|
||
item["type"] = (Json::UInt64)type;
|
||
item["typeName"] = ToUtf8(typeName, cp);
|
||
item["fileSystem"] = ToUtf8(fileSystem, cp);
|
||
item["totalMB"] = (Json::UInt64)totalMB;
|
||
item["freeMB"] = (Json::UInt64)freeMB;
|
||
arr.append(item);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 TOKEN_FILE_LIST 缓冲(data[0]=token,其后为
|
||
// [attr:1][filename\0][sizeHigh:4][sizeLow:4][ftLastWriteTime:8] 记录)。
|
||
// 文件名为客户端 ANSI(编码由 cp 指定);attr 非 0 表示目录(FILE_ATTRIBUTE_DIRECTORY)。
|
||
Json::Value ParseFileList(const std::vector<BYTE>& data, int maxEntries, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 2) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t len = data.size();
|
||
size_t off = 1;
|
||
int count = 0;
|
||
while (off + 1 <= len && count < maxEntries) {
|
||
bool isDir = (p[off] & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||
off += 1;
|
||
|
||
const char* name = p + off;
|
||
size_t nlen = BoundedStrlen(name, len - off);
|
||
if (nlen >= len - off) break;
|
||
if (nlen == 0) break; // 空记录 = 尾部零填充(LocalAlloc 对齐),停止解析
|
||
off += nlen + 1;
|
||
|
||
if (off + 16 > len) break;
|
||
DWORD sizeHigh = *(const DWORD*)(p + off);
|
||
DWORD sizeLow = *(const DWORD*)(p + off + 4);
|
||
ULONGLONG size = ((ULONGLONG)sizeHigh << 32) | sizeLow;
|
||
// 修改时间(FILETIME,100ns since 1601-01-01)→ Unix 秒
|
||
// FILETIME 在线上是 dwLowDateTime(低 4 字节) 在前、dwHighDateTime(高 4 字节) 在后
|
||
// (客户端 memcpy(&ftLastWriteTime, sizeof(FILETIME)),见 FileManager.cpp::SendFilesList)。
|
||
DWORD ftLow = *(const DWORD*)(p + off + 8);
|
||
DWORD ftHigh = *(const DWORD*)(p + off + 12);
|
||
off += 16;
|
||
|
||
ULONGLONG ft = ((ULONGLONG)ftHigh << 32) | ftLow;
|
||
Json::Int64 mtime = (Json::Int64)(ft / 10000000ULL) - 11644473600LL;
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["name"] = ToUtf8(name, cp);
|
||
item["isDir"] = isDir;
|
||
item["size"] = (Json::UInt64)size;
|
||
item["mtime"] = mtime;
|
||
arr.append(item);
|
||
++count;
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 TOKEN_SERVERLIST 缓冲(data[0]=token,其后为
|
||
// [displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0] 记录)。
|
||
// 字段全部来自 Windows A 接口(EnumServicesStatus / QueryServiceConfig),为客户端
|
||
// ANSI(编码由 cp 指定)。以 displayName 与 serviceName 同时为空作为尾部零填充
|
||
// (LocalAlloc/LocalReAlloc LMEM_ZEROINIT 对齐)的终止条件。
|
||
Json::Value ParseServiceList(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 2) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t len = data.size();
|
||
size_t off = 1; // 跳过 TOKEN 字节
|
||
while (off < len) {
|
||
const char* f[5];
|
||
bool ok = true;
|
||
for (int i = 0; i < 5; ++i) {
|
||
size_t n = BoundedStrlen(p + off, len - off);
|
||
if (n >= len - off) { ok = false; break; } // 未以 '\0' 结尾,异常数据
|
||
f[i] = p + off;
|
||
off += n + 1;
|
||
}
|
||
if (!ok) break;
|
||
// displayName 与 serviceName 都为空 → 尾部零填充,停止解析
|
||
if (f[0][0] == '\0' && f[1][0] == '\0') break;
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["display_name"] = ToUtf8(f[0], cp);
|
||
item["service_name"] = ToUtf8(f[1], cp);
|
||
item["binary_path"] = ToUtf8(f[2], cp);
|
||
item["status"] = ToUtf8(f[3], cp); // Stopped/Running/Paused/... 英文
|
||
item["start_type"] = ToUtf8(f[4], cp); // Boot-Start/Auto-Start/Demand-Start/... 英文
|
||
arr.append(item);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// ========== list_registry:注册表解析 ==========
|
||
|
||
// 注册表 REGMSG 头(client RegisterOperation.cpp / server RegisterDlg.cpp 各自本地定义,此处同构)。
|
||
struct RegMsgHeader {
|
||
int count; // 名字个数
|
||
DWORD size; // 名字大小(定宽)
|
||
DWORD valsize; // 值大小(定宽)
|
||
};
|
||
|
||
// client KEYVALUE 枚举(RegisterOperation.cpp):MREG_SZ=0 … MREG_NONE=6。
|
||
enum RegValueType {
|
||
REG_T_SZ = 0,
|
||
REG_T_DWORD = 1,
|
||
REG_T_BINARY = 2,
|
||
REG_T_EXPAND_SZ = 3,
|
||
REG_T_MULTI_SZ = 4,
|
||
REG_T_QWORD = 5,
|
||
REG_T_NONE = 6,
|
||
};
|
||
|
||
// client MYKEY 枚举:根键 token。
|
||
enum RegRootToken {
|
||
REG_ROOT_CLASSES_ROOT = 0,
|
||
REG_ROOT_CURRENT_USER = 1,
|
||
REG_ROOT_LOCAL_MACHINE = 2,
|
||
REG_ROOT_USERS = 3,
|
||
REG_ROOT_CURRENT_CONFIG = 4,
|
||
};
|
||
|
||
// ASCII 不区分大小写前缀匹配(注册表根键名大小写不敏感)。
|
||
bool StrPrefixCI(const std::string& s, const char* prefix, size_t len) {
|
||
if (s.size() < len) return false;
|
||
for (size_t i = 0; i < len; ++i) {
|
||
char a = s[i], b = prefix[i];
|
||
if (a >= 'A' && a <= 'Z') a += 32;
|
||
if (b >= 'A' && b <= 'Z') b += 32;
|
||
if (a != b) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 值类型名(可读字符串)
|
||
const char* RegTypeName(BYTE type) {
|
||
switch (type) {
|
||
case REG_T_SZ: return "REG_SZ";
|
||
case REG_T_DWORD: return "REG_DWORD";
|
||
case REG_T_BINARY: return "REG_BINARY";
|
||
case REG_T_EXPAND_SZ: return "REG_EXPAND_SZ";
|
||
case REG_T_MULTI_SZ: return "REG_MULTI_SZ";
|
||
case REG_T_QWORD: return "REG_QWORD";
|
||
case REG_T_NONE: return "REG_NONE";
|
||
default: return "REG_UNKNOWN";
|
||
}
|
||
}
|
||
|
||
// 将注册表值数据格式化为字符串。valSize 为定宽(client 按 MaxDataLen+1 填充),
|
||
// 字符串类取到 '\0' 为止、数值类取定长前缀;REG_BINARY 长度无法从协议还原,按 valSize 输出 hex。
|
||
std::string FormatRegData(BYTE type, const BYTE* val, size_t valSize, UINT cp) {
|
||
switch (type) {
|
||
case REG_T_SZ:
|
||
case REG_T_EXPAND_SZ:
|
||
return ToUtf8((const char*)val, cp);
|
||
case REG_T_MULTI_SZ: { // 多个 '\0' 结尾串、双 '\0' 结束
|
||
std::string out;
|
||
size_t i = 0;
|
||
while (i < valSize && val[i] != '\0') {
|
||
const char* s = (const char*)(val + i);
|
||
size_t n = BoundedStrlen(s, valSize - i);
|
||
if (!out.empty()) out += "\n";
|
||
out += ToUtf8(s, cp);
|
||
i += n + 1;
|
||
}
|
||
return out;
|
||
}
|
||
case REG_T_DWORD: { // 4 字节小端
|
||
if (valSize < 4) return "";
|
||
DWORD v = 0;
|
||
memcpy(&v, val, 4);
|
||
char buf[64];
|
||
sprintf(buf, "0x%08lX (%lu)", (unsigned long)v, (unsigned long)v);
|
||
return buf;
|
||
}
|
||
case REG_T_QWORD: { // 8 字节小端
|
||
if (valSize < 8) return "";
|
||
uint64_t v = 0;
|
||
memcpy(&v, val, 8);
|
||
char buf[96];
|
||
sprintf(buf, "0x%016I64X (%I64u)", (unsigned __int64)v, (unsigned __int64)v);
|
||
return buf;
|
||
}
|
||
case REG_T_BINARY: { // hex,尾部可能含 0 填充
|
||
std::string out;
|
||
char buf[4];
|
||
for (size_t i = 0; i < valSize; ++i) {
|
||
sprintf(buf, "%02X", val[i]);
|
||
out += buf;
|
||
}
|
||
return out;
|
||
}
|
||
case REG_T_NONE:
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
// 解析 TOKEN_REG_PATH 缓冲:[token:1][RegMsgHeader:12][count * size 定宽子键名]。cp 为客户端 ANSI。
|
||
Json::Value ParseRegPath(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t off = 1; // 跳过 TOKEN 字节
|
||
RegMsgHeader hdr;
|
||
memcpy(&hdr, p + off, sizeof(hdr));
|
||
off += sizeof(hdr);
|
||
int count = hdr.count;
|
||
size_t nameSize = hdr.size;
|
||
for (int i = 0; i < count; ++i) {
|
||
if (off + nameSize > data.size()) break;
|
||
const char* name = p + off;
|
||
off += nameSize;
|
||
if (name[0] == '\0') break; // 空名 = 尾部零填充,停止
|
||
arr.append(ToUtf8(name, cp));
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 TOKEN_REG_KEY 缓冲:[token:1][RegMsgHeader:12][count * {type:1,name:size,data:valsize}]。
|
||
Json::Value ParseRegKey(const std::vector<BYTE>& data, UINT cp) {
|
||
Json::Value arr(Json::arrayValue);
|
||
if (data.size() < 1 + sizeof(RegMsgHeader)) return arr;
|
||
const char* p = (const char*)data.data();
|
||
size_t off = 1; // 跳过 TOKEN 字节
|
||
RegMsgHeader hdr;
|
||
memcpy(&hdr, p + off, sizeof(hdr));
|
||
off += sizeof(hdr);
|
||
int count = hdr.count;
|
||
size_t nameSize = hdr.size;
|
||
size_t valSize = hdr.valsize;
|
||
for (int i = 0; i < count; ++i) {
|
||
if (off + 1 + nameSize + valSize > data.size()) break;
|
||
BYTE type = (BYTE)p[off];
|
||
off += 1;
|
||
const char* name = p + off; // 定宽 nameSize 内以 '\0' 结尾
|
||
off += nameSize;
|
||
const BYTE* val = (const BYTE*)(p + off);
|
||
off += valSize;
|
||
|
||
Json::Value item(Json::objectValue);
|
||
item["name"] = ToUtf8(name, cp);
|
||
item["type"] = RegTypeName(type);
|
||
item["data"] = FormatRegData(type, val, valSize, cp);
|
||
arr.append(item);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
// 解析 "HKEY_LOCAL_MACHINE\Software\..." → rootToken + 相对子键路径(去掉根键名与首个 '\')。
|
||
// 返回 false 表示 path 为空(调用方列根键)或未匹配任何根键(调用方报错)。
|
||
bool ParseRegistryPath(const std::string& path, BYTE& rootToken, std::string& relPath) {
|
||
struct RootMap { const char* name; BYTE token; };
|
||
static const RootMap roots[] = {
|
||
{ "HKEY_CLASSES_ROOT", REG_ROOT_CLASSES_ROOT },
|
||
{ "HKEY_CURRENT_USER", REG_ROOT_CURRENT_USER },
|
||
{ "HKEY_LOCAL_MACHINE", REG_ROOT_LOCAL_MACHINE },
|
||
{ "HKEY_USERS", REG_ROOT_USERS },
|
||
{ "HKEY_CURRENT_CONFIG", REG_ROOT_CURRENT_CONFIG },
|
||
};
|
||
if (path.empty()) return false;
|
||
for (const RootMap& r : roots) {
|
||
size_t n = strlen(r.name);
|
||
if (StrPrefixCI(path, r.name, n)) {
|
||
rootToken = r.token;
|
||
relPath = path.substr(n);
|
||
if (!relPath.empty() && relPath[0] == '\\') relPath.erase(0, 1);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 列根键(path 为空):返回 5 个固定根键,无需查询客户端。
|
||
std::string BuildRegistryRoots(const Json::Value& id) {
|
||
Json::Value keys(Json::arrayValue);
|
||
const char* roots[] = {
|
||
"HKEY_CLASSES_ROOT", "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE",
|
||
"HKEY_USERS", "HKEY_CURRENT_CONFIG"
|
||
};
|
||
for (const char* r : roots) keys.append(r);
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["keys"] = keys;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string((int)keys.size()) + std::string(u8" 个根键。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// 收集所有在线主机 JSON 数组(m_cs 锁内遍历,复用 BuildHostJson 序列化,方案 C)
|
||
void CollectOnlineHosts(CMy2015RemoteDlg* parent, Json::Value& hosts) {
|
||
if (!parent) return;
|
||
EnterCriticalSection(&parent->m_cs);
|
||
for (context* ctx : parent->m_HostList) {
|
||
if (!ctx || !ctx->IsLogin()) continue;
|
||
hosts.append(BuildHostJson(ctx, parent->m_ClientMap));
|
||
}
|
||
LeaveCriticalSection(&parent->m_cs);
|
||
}
|
||
|
||
// ========== 工具 schema ==========
|
||
|
||
// 单台主机字段 schema(hosts 数组元素 / 单机详情共用的形状)
|
||
Json::Value BuildHostItemSchema() {
|
||
Json::Value itemProps(Json::objectValue);
|
||
const char* strFields[] = {
|
||
"id", "name", "remark", "ip", "os", "location", "rtt",
|
||
"version", "activeWindow", "group", "screen", "clientType"
|
||
};
|
||
for (const char* f : strFields) {
|
||
Json::Value p(Json::objectValue);
|
||
p["type"] = "string";
|
||
itemProps[f] = p;
|
||
}
|
||
Json::Value onlineProp(Json::objectValue);
|
||
onlineProp["type"] = "boolean";
|
||
itemProps["online"] = onlineProp;
|
||
return itemProps;
|
||
}
|
||
|
||
// list_online_hosts / search_hosts 的 outputSchema(hosts 数组)
|
||
Json::Value BuildHostOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value hostsProp(Json::objectValue);
|
||
hostsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
items["properties"] = BuildHostItemSchema();
|
||
hostsProp["items"] = items;
|
||
props["hosts"] = hostsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("hosts");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_host_detail 的 outputSchema(单台主机)
|
||
Json::Value BuildHostDetailOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value hostProp(Json::objectValue);
|
||
hostProp["type"] = "object";
|
||
hostProp["properties"] = BuildHostItemSchema();
|
||
props["host"] = hostProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("host");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// search_hosts 的 inputSchema(全部可选)
|
||
Json::Value BuildSearchHostsInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
const char* strParams[] = { "name", "ip", "group", "os" };
|
||
for (const char* p : strParams) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
props[p] = s;
|
||
}
|
||
Json::Value onlineProp(Json::objectValue);
|
||
onlineProp["type"] = "boolean";
|
||
props["online"] = onlineProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// get_host_detail 的 inputSchema(id 必填)
|
||
Json::Value BuildGetHostDetailInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value idProp(Json::objectValue);
|
||
idProp["type"] = "string";
|
||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||
props["id"] = idProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("id");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_processes 的 outputSchema(processes 数组)
|
||
Json::Value BuildProcessListOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value procsProp(Json::objectValue);
|
||
procsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
Json::Value itemProps(Json::objectValue);
|
||
Json::Value pidProp(Json::objectValue);
|
||
pidProp["type"] = "integer";
|
||
itemProps["pid"] = pidProp;
|
||
const char* strFields[] = { "name", "arch", "path" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
itemProps[f] = s;
|
||
}
|
||
items["properties"] = itemProps;
|
||
procsProp["items"] = items;
|
||
props["processes"] = procsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("processes");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_windows 的 outputSchema(windows 数组)
|
||
Json::Value BuildWindowListOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value winsProp(Json::objectValue);
|
||
winsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
Json::Value itemProps(Json::objectValue);
|
||
Json::Value hwndProp(Json::objectValue);
|
||
hwndProp["type"] = "integer";
|
||
itemProps["hwnd"] = hwndProp;
|
||
Json::Value pidProp(Json::objectValue);
|
||
pidProp["type"] = "integer";
|
||
itemProps["pid"] = pidProp;
|
||
const char* strFields[] = { "title", "status" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
itemProps[f] = s;
|
||
}
|
||
items["properties"] = itemProps;
|
||
winsProp["items"] = items;
|
||
props["windows"] = winsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("windows");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_activity_history 的 outputSchema(records 数组 + 原始文本)
|
||
Json::Value BuildActivityHistoryOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value recordsProp(Json::objectValue);
|
||
recordsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "string";
|
||
recordsProp["items"] = items;
|
||
props["records"] = recordsProp;
|
||
|
||
Json::Value rawProp(Json::objectValue);
|
||
rawProp["type"] = "string";
|
||
props["activityHistory"] = rawProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// list_services 的 outputSchema(services 数组)
|
||
Json::Value BuildServiceListOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value svcsProp(Json::objectValue);
|
||
svcsProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
Json::Value itemProps(Json::objectValue);
|
||
const char* strFields[] = { "display_name", "service_name", "binary_path", "status", "start_type" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
itemProps[f] = s;
|
||
}
|
||
items["properties"] = itemProps;
|
||
svcsProp["items"] = items;
|
||
props["services"] = svcsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("services");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
|
||
Json::Value BuildListRegistryInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value idProp(Json::objectValue);
|
||
idProp["type"] = "string";
|
||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||
props["id"] = idProp;
|
||
|
||
Json::Value pathProp(Json::objectValue);
|
||
pathProp["type"] = "string";
|
||
pathProp["description"] = u8"注册表键路径(如 HKEY_LOCAL_MACHINE\\Software);省略或传空则返回 5 个根键";
|
||
props["path"] = pathProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("id");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_registry 的 outputSchema(keys 字符串数组 / values 对象数组)
|
||
Json::Value BuildListRegistryOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value pathProp(Json::objectValue);
|
||
pathProp["type"] = "string";
|
||
props["path"] = pathProp;
|
||
|
||
Json::Value keysProp(Json::objectValue);
|
||
keysProp["type"] = "array";
|
||
Json::Value keyItems(Json::objectValue);
|
||
keyItems["type"] = "string";
|
||
keysProp["items"] = keyItems;
|
||
props["keys"] = keysProp;
|
||
|
||
Json::Value valsProp(Json::objectValue);
|
||
valsProp["type"] = "array";
|
||
Json::Value valItems(Json::objectValue);
|
||
valItems["type"] = "object";
|
||
Json::Value valProps(Json::objectValue);
|
||
const char* strFields[] = { "name", "type", "data" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
valProps[f] = s;
|
||
}
|
||
valItems["properties"] = valProps;
|
||
valsProp["items"] = valItems;
|
||
props["values"] = valsProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("keys");
|
||
required.append("values");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// exec_command 的 inputSchema(id 必填、command 必填、timeout_ms 可选)
|
||
Json::Value BuildExecCommandInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value idProp(Json::objectValue);
|
||
idProp["type"] = "string";
|
||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||
props["id"] = idProp;
|
||
|
||
Json::Value cmdProp(Json::objectValue);
|
||
cmdProp["type"] = "string";
|
||
cmdProp["description"] = u8"要执行的命令(受只读模式与命令白名单约束,仅允许只读命令前缀)";
|
||
props["command"] = cmdProp;
|
||
|
||
Json::Value timeoutProp(Json::objectValue);
|
||
timeoutProp["type"] = "integer";
|
||
timeoutProp["description"] = u8"等待输出完成的超时毫秒数(可选,默认 20000,上限 600000)";
|
||
props["timeout_ms"] = timeoutProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("id");
|
||
required.append("command");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// exec_command 的 outputSchema(stdout 文本 / exit_code)
|
||
Json::Value BuildExecCommandOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value stdoutProp(Json::objectValue);
|
||
stdoutProp["type"] = "string";
|
||
stdoutProp["description"] = u8"命令输出(已剥哨兵与 ANSI 转义)";
|
||
props["stdout"] = stdoutProp;
|
||
|
||
Json::Value exitProp(Json::objectValue);
|
||
exitProp["type"] = "integer";
|
||
exitProp["description"] = u8"退出码:0=成功、1=非零退出、-1=未知(进程异常退出)";
|
||
props["exit_code"] = exitProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("stdout");
|
||
required.append("exit_code");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_client_log 的 outputSchema(原始日志文本)
|
||
Json::Value BuildClientLogOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value logProp(Json::objectValue);
|
||
logProp["type"] = "string";
|
||
props["log"] = logProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("log");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_audit_log 的 outputSchema(服务端消息/审计日志条目数组)
|
||
Json::Value BuildAuditLogOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value entriesProp(Json::objectValue);
|
||
entriesProp["type"] = "array";
|
||
Json::Value items(Json::objectValue);
|
||
items["type"] = "object";
|
||
Json::Value itemProps(Json::objectValue);
|
||
const char* strFields[] = { "type", "time", "msg" };
|
||
for (const char* f : strFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
itemProps[f] = s;
|
||
}
|
||
items["properties"] = itemProps;
|
||
entriesProp["items"] = items;
|
||
props["entries"] = entriesProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("entries");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_screenshot 的 outputSchema(image 元数据;base64 数据在 content 的 image 块中)
|
||
Json::Value BuildScreenshotOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value imageProp(Json::objectValue);
|
||
imageProp["type"] = "object";
|
||
Json::Value imageProps(Json::objectValue);
|
||
Json::Value mimeProp(Json::objectValue);
|
||
mimeProp["type"] = "string";
|
||
imageProps["mimeType"] = mimeProp;
|
||
const char* intFields[] = { "width", "height", "bytes" };
|
||
for (const char* f : intFields) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "integer";
|
||
imageProps[f] = s;
|
||
}
|
||
imageProp["properties"] = imageProps;
|
||
props["image"] = imageProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("image");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// get_screenshot 的 inputSchema(id 必填,max_width 可选)
|
||
Json::Value BuildGetScreenshotInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value idProp(Json::objectValue);
|
||
idProp["type"] = "string";
|
||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||
props["id"] = idProp;
|
||
|
||
Json::Value mwProp(Json::objectValue);
|
||
mwProp["type"] = "integer";
|
||
mwProp["description"] = u8"期望图片最大宽度(像素,钳制到 64~1920)。省略或传 0 时沿用 RTT 自适应缩略图档位(最大 1024);传 1920 可拿到接近原分辨率(1080p 源屏即原分辨率,4K 源屏最多 1920),供 AI 视觉/OCR 场景提升清晰度。";
|
||
props["max_width"] = mwProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("id");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_files 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列盘)
|
||
Json::Value BuildListFilesInputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value idProp(Json::objectValue);
|
||
idProp["type"] = "string";
|
||
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
|
||
props["id"] = idProp;
|
||
|
||
Json::Value pathProp(Json::objectValue);
|
||
pathProp["type"] = "string";
|
||
pathProp["description"] = u8"要列举的目录路径(如 C:\\Windows);省略或传空则返回驱动器列表";
|
||
props["path"] = pathProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
Json::Value required(Json::arrayValue);
|
||
required.append("id");
|
||
schema["required"] = required;
|
||
return schema;
|
||
}
|
||
|
||
// list_files 的 outputSchema(drives 数组 / files 数组,二者其一非空)
|
||
Json::Value BuildListFilesOutputSchema() {
|
||
Json::Value props(Json::objectValue);
|
||
|
||
Json::Value drivesProp(Json::objectValue);
|
||
drivesProp["type"] = "array";
|
||
Json::Value driveItems(Json::objectValue);
|
||
driveItems["type"] = "object";
|
||
Json::Value driveProps(Json::objectValue);
|
||
const char* driveStr[] = { "drive", "typeName", "fileSystem" };
|
||
for (const char* f : driveStr) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "string";
|
||
driveProps[f] = s;
|
||
}
|
||
const char* driveInt[] = { "type", "totalMB", "freeMB" };
|
||
for (const char* f : driveInt) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "integer";
|
||
driveProps[f] = s;
|
||
}
|
||
driveItems["properties"] = driveProps;
|
||
drivesProp["items"] = driveItems;
|
||
props["drives"] = drivesProp;
|
||
|
||
Json::Value filesProp(Json::objectValue);
|
||
filesProp["type"] = "array";
|
||
Json::Value fileItems(Json::objectValue);
|
||
fileItems["type"] = "object";
|
||
Json::Value fileProps(Json::objectValue);
|
||
Json::Value nameProp(Json::objectValue);
|
||
nameProp["type"] = "string";
|
||
fileProps["name"] = nameProp;
|
||
Json::Value isDirProp(Json::objectValue);
|
||
isDirProp["type"] = "boolean";
|
||
fileProps["isDir"] = isDirProp;
|
||
const char* fileInt[] = { "size", "mtime" };
|
||
for (const char* f : fileInt) {
|
||
Json::Value s(Json::objectValue);
|
||
s["type"] = "integer";
|
||
fileProps[f] = s;
|
||
}
|
||
fileItems["properties"] = fileProps;
|
||
filesProp["items"] = fileItems;
|
||
props["files"] = filesProp;
|
||
|
||
Json::Value schema(Json::objectValue);
|
||
schema["type"] = "object";
|
||
schema["properties"] = props;
|
||
return schema;
|
||
}
|
||
|
||
// tools/list
|
||
std::string BuildToolsListResult(const Json::Value& id) {
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value tools(Json::arrayValue);
|
||
|
||
// 1) list_online_hosts
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_online_hosts";
|
||
// 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK,
|
||
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
|
||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
|
||
|
||
Json::Value inputSchema(Json::objectValue);
|
||
inputSchema["type"] = "object";
|
||
inputSchema["properties"] = Json::Value(Json::objectValue);
|
||
inputSchema["required"] = Json::Value(Json::arrayValue);
|
||
tool["inputSchema"] = inputSchema;
|
||
|
||
tool["outputSchema"] = BuildHostOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 2) search_hosts(P2a:纯内存过滤,无子链接)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "search_hosts";
|
||
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。";
|
||
|
||
tool["inputSchema"] = BuildSearchHostsInputSchema();
|
||
tool["outputSchema"] = BuildHostOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 3) get_host_detail(P2a:单机详情,纯内存)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_host_detail";
|
||
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema();
|
||
tool["outputSchema"] = BuildHostDetailOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 4) list_processes(P2b:主连接 RPC,进程列表)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_processes";
|
||
tool["description"] = u8"获取指定在线主机的进程列表(PID、映像名称、架构、程序完整路径)。通过主连接下发命令、子连接回传,一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildProcessListOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 5) list_windows(P2b:主连接 RPC,窗口列表)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_windows";
|
||
tool["description"] = u8"获取指定在线主机的顶层窗口列表(句柄、窗口标题、窗口状态、所属进程 PID)。一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildWindowListOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 6) get_activity_history(P2b:主连接 RPC,历史活动记录)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_activity_history";
|
||
tool["description"] = u8"获取指定在线主机的历史活动记录(前台窗口驻留时长,每行一条「[时间] [标题] 时长」)。主连接 RPC、一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildActivityHistoryOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 7) get_screenshot(P2c:屏幕预览链路,主连接 RPC)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_screenshot";
|
||
tool["description"] = u8"截取指定在线主机的一帧屏幕,返回 base64 编码的 JPEG 图片(image 对象含 mimeType/data/width/height)。仅 Windows 客户端支持(能力位 CLIENT_CAP_SCREEN_PREVIEW),主连接 RPC、一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetScreenshotInputSchema(); // { id } 必填 + max_width 可选
|
||
tool["outputSchema"] = BuildScreenshotOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 8) list_files(P2c:文件链路,列盘/列目录)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_files";
|
||
tool["description"] = u8"列举指定在线主机的目录。省略或空 path 返回驱动器列表(drives);给定 path 返回该目录一层内的文件/子目录(files,最多 500 条,含 name/isDir/size/mtime)。只读。";
|
||
|
||
tool["inputSchema"] = BuildListFilesInputSchema();
|
||
tool["outputSchema"] = BuildListFilesOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 9) list_services(P3:服务链路,仅 Windows,一次性子链接)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_services";
|
||
tool["description"] = u8"获取指定在线 Windows 主机的服务列表(显示名、服务名、可执行文件路径、运行状态、启动类型)。仅 Windows 客户端支持;一次性返回。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildServiceListOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 10) get_client_log(P3:客户端运行日志,一次性子链接取首条全量)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_client_log";
|
||
tool["description"] = u8"获取指定在线主机 YAMA 客户端的内存运行日志(最近最多 1000 条,含时间戳/源文件/行号)。客户端持续增量上报,本工具取当前时刻的全量快照后即断开。只读。";
|
||
|
||
tool["inputSchema"] = BuildGetHostDetailInputSchema(); // 复用 { id } 必填 schema
|
||
tool["outputSchema"] = BuildClientLogOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 11) get_audit_log(服务端本地:主界面消息/审计日志,纯内存,无子链接)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "get_audit_log";
|
||
tool["description"] = u8"获取 YAMA 服务端主界面的消息/审计日志(主机上线/下线、操作结果、告警等)。返回列表内当前全部条目(最多 1000 条,新在前)。只读、纯内存。";
|
||
|
||
Json::Value inputSchema(Json::objectValue);
|
||
inputSchema["type"] = "object";
|
||
inputSchema["properties"] = Json::Value(Json::objectValue);
|
||
inputSchema["required"] = Json::Value(Json::arrayValue);
|
||
tool["inputSchema"] = inputSchema;
|
||
|
||
tool["outputSchema"] = BuildAuditLogOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 12) list_registry(P3:注册表查询,列根键/列子键+值,仅 Windows,一次性子链接)
|
||
{
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "list_registry";
|
||
tool["description"] = u8"查询指定在线 Windows 主机的注册表。省略或空 path 返回 5 个根键;给定 path(如 HKEY_LOCAL_MACHINE\\Software)返回该键一层内的子键(keys)与值(values,含 name/type/data)。只读。";
|
||
|
||
tool["inputSchema"] = BuildListRegistryInputSchema();
|
||
tool["outputSchema"] = BuildListRegistryOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
// 13) exec_command(P3:一次性远程命令,仅 Windows,安全门:只读默认 + 白名单 + 审计)
|
||
// 只读模式下 tools/list 直接隐藏该工具;即便被绕过,BuildExecCommand 也会再次校验只读。
|
||
if (!CMcpServer::Instance().IsReadonly()) {
|
||
Json::Value tool(Json::objectValue);
|
||
tool["name"] = "exec_command";
|
||
tool["description"] = u8"在指定在线 Windows 主机上执行一条命令并返回 stdout 与退出码(受只读模式与命令白名单约束,默认仅允许只读命令前缀,见 McpReadonly / McpCmdWhitelist)。";
|
||
|
||
tool["inputSchema"] = BuildExecCommandInputSchema();
|
||
tool["outputSchema"] = BuildExecCommandOutputSchema();
|
||
|
||
tools.append(tool);
|
||
}
|
||
|
||
result["tools"] = tools;
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_online_hosts
|
||
std::string BuildListOnlineHosts(const Json::Value& id, CMy2015RemoteDlg* parent) {
|
||
Json::Value hosts(Json::arrayValue);
|
||
CollectOnlineHosts(parent, hosts);
|
||
int count = (int)hosts.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["hosts"] = hosts;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 台主机在线。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:search_hosts
|
||
std::string BuildSearchHosts(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
Json::Value all(Json::arrayValue);
|
||
CollectOnlineHosts(parent, all);
|
||
|
||
std::string fName = GetStringArg(args, "name");
|
||
std::string fIp = GetStringArg(args, "ip");
|
||
std::string fGroup = GetStringArg(args, "group");
|
||
std::string fOs = GetStringArg(args, "os");
|
||
bool hasOnline = args.isObject() && args.isMember("online") && args["online"].isBool();
|
||
bool wantOnline = hasOnline ? args["online"].asBool() : true;
|
||
|
||
Json::Value hosts(Json::arrayValue);
|
||
// 列表只含在线主机:显式 online=false 时直接空结果
|
||
if (!hasOnline || wantOnline) {
|
||
for (unsigned int i = 0; i < all.size(); ++i) {
|
||
const Json::Value& h = all[i];
|
||
if (!fName.empty()) {
|
||
std::string name = JsonStrField(h, "name");
|
||
std::string remark = JsonStrField(h, "remark");
|
||
if (!ContainsCI(name, fName) && !ContainsCI(remark, fName)) continue;
|
||
}
|
||
if (!fIp.empty() && !ContainsCI(JsonStrField(h, "ip"), fIp)) continue;
|
||
if (!fGroup.empty() && !ContainsCI(JsonStrField(h, "group"), fGroup)) continue;
|
||
if (!fOs.empty() && !ContainsCI(JsonStrField(h, "os"), fOs)) continue;
|
||
hosts.append(h);
|
||
}
|
||
}
|
||
int count = (int)hosts.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["hosts"] = hosts;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 台主机匹配。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_host_detail
|
||
std::string BuildGetHostDetail(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
std::string sid = GetStringArg(args, "id");
|
||
if (sid.empty()) {
|
||
return BuildError(id, -32602, "Missing required parameter: id");
|
||
}
|
||
if (!IsDigits(sid)) {
|
||
return BuildError(id, -32602, "Invalid id: expected a decimal host id string");
|
||
}
|
||
|
||
Json::Value all(Json::arrayValue);
|
||
CollectOnlineHosts(parent, all);
|
||
for (unsigned int i = 0; i < all.size(); ++i) {
|
||
const Json::Value& h = all[i];
|
||
if (JsonStrField(h, "id") == sid) {
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["host"] = h;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
std::string name = JsonStrField(h, "name");
|
||
std::string ip = JsonStrField(h, "ip");
|
||
item["text"] = std::string(u8"主机 ") + name + " (" + ip + ")" + u8" 的详情。";
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
}
|
||
|
||
return BuildError(id, -32002, "Host not found or offline: " + sid);
|
||
}
|
||
|
||
// tools/call:list_processes(主连接 RPC,子连接一次性回传)
|
||
std::string BuildListProcesses(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_processes"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_SYSTEM;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for process list");
|
||
|
||
// 进程名/路径编码按 clientType 判定:Windows 走 A 接口(QueryFullProcessImageNameA 等)
|
||
// =客户端 ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 的 /proc 天然 UTF-8。
|
||
// 不能用 GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 进程名。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
|
||
Json::Value procs = ParseProcessList(data, cp);
|
||
int count = (int)procs.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["processes"] = procs;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个进程。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_windows(主连接 RPC,子连接一次性回传)
|
||
std::string BuildListWindows(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_windows"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_WSLIST;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for window list");
|
||
|
||
// 窗口标题编码由客户端能力位决定(新客户端 UTF-8,老客户端 CP_ACP)。
|
||
UINT cp = GetClientEncoding(ctx);
|
||
Json::Value wins = ParseWindowList(data, cp);
|
||
int count = (int)wins.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["windows"] = wins;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个窗口。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_activity_history(主连接 RPC,无子链接、不弹框、不 CancelIO)
|
||
std::string BuildGetActivityHistory(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "get_activity_history"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_QUERY_ACTIVITY;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for activity history");
|
||
|
||
// data[0]=token,其后为客户端 ActivityHistory::Dump() 的 UTF-8 纯文本
|
||
// (标题在客户端已由 GetActiveWindowTitle 转 UTF-8,服务端无需再转码)。
|
||
std::string text;
|
||
if (data.size() > 1)
|
||
text.assign((const char*)data.data() + 1, data.size() - 1);
|
||
|
||
// 拆行:每行一条记录,过滤空行(含 Dump 末尾的换行)。
|
||
Json::Value records(Json::arrayValue);
|
||
size_t start = 0;
|
||
while (start <= text.size()) {
|
||
size_t nl = text.find('\n', start);
|
||
std::string line = text.substr(start, (nl == std::string::npos ? text.size() : nl) - start);
|
||
if (!line.empty()) records.append(line);
|
||
if (nl == std::string::npos) break;
|
||
start = nl + 1;
|
||
}
|
||
int count = (int)records.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["records"] = records;
|
||
structuredContent["activityHistory"] = text;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条历史活动记录。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_screenshot(屏幕预览链路,主连接 RPC,不建子链接、不弹框)
|
||
std::string BuildGetScreenshot(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
// 能力位门槛:仅 Windows 客户端声明 CLIENT_CAP_SCREEN_PREVIEW(非 Windows 返回明确错误)
|
||
if (!ctx->SupportsScreenPreview())
|
||
return BuildError(id, -32005, "Host does not support screen preview");
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "get_screenshot"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
uint16_t reqId = mcp.NextPreviewReqId();
|
||
mcp.SetPendingReqId(devId, reqId);
|
||
|
||
// 复用 MFC 预览的 RTT/FRP 自适应参数挑选,避免重复实现 GetTargetQualityLevel 逻辑
|
||
WORD maxWidth = 0;
|
||
BYTE quality = 0;
|
||
parent->ChooseScreenPreviewParams(ctx, maxWidth, quality);
|
||
|
||
// 可选 max_width:缺省/0 沿用缩略图档位;>0 覆盖宽度(钳制到客户端上限 [64,1920]),
|
||
// 供 AI 视觉/OCR 场景请求接近原分辨率的大图。jpegQuality 仍沿用档位自适应值。
|
||
if (args.isMember("max_width") && args["max_width"].isInt()) {
|
||
int mw = args["max_width"].asInt();
|
||
if (mw > 0) {
|
||
if (mw < 64) mw = 64;
|
||
if (mw > 1920) mw = 1920;
|
||
maxWidth = (WORD)mw;
|
||
}
|
||
}
|
||
|
||
parent->SendScreenPreviewRequest(ctx, reqId, maxWidth, quality);
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for screenshot");
|
||
|
||
// data = [ScreenPreviewRspHeader][JPEG]
|
||
if (data.size() < sizeof(ScreenPreviewRspHeader))
|
||
return BuildError(id, -32000, "Invalid screenshot response");
|
||
|
||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(data.data());
|
||
if (hdr->status != SCREEN_PREVIEW_OK || hdr->format != SCREEN_PREVIEW_FMT_JPEG ||
|
||
hdr->bytes == 0 || data.size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes) {
|
||
return BuildError(id, -32000,
|
||
"Screenshot capture failed (status " + std::to_string((int)hdr->status) + ")");
|
||
}
|
||
|
||
std::string b64 = httplib::detail::base64_encode(
|
||
std::string((const char*)data.data() + sizeof(ScreenPreviewRspHeader), hdr->bytes));
|
||
|
||
Json::Value image(Json::objectValue);
|
||
image["mimeType"] = "image/jpeg";
|
||
image["width"] = (Json::UInt64)hdr->width;
|
||
image["height"] = (Json::UInt64)hdr->height;
|
||
image["bytes"] = (Json::UInt64)hdr->bytes;
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["image"] = image; // 元数据(不含 base64,避免在 content 之外重复大 payload)
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "image";
|
||
item["data"] = b64;
|
||
item["mimeType"] = "image/jpeg";
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_files(列盘走 COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST;
|
||
// 列目录走 COMMAND_LIST_DRIVE 开子链接后,再下发 COMMAND_LIST_FILES→TOKEN_FILE_LIST)
|
||
std::string BuildListFiles(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
std::string path = GetStringArg(args, "path");
|
||
bool listDrives = path.empty() || path == "." || path == "/" || path == "\\";
|
||
|
||
// 目录路径/文件名编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA 等)= 客户端
|
||
// ANSI(GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 文件系统天然 UTF-8。不能用
|
||
// GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 文件名。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
|
||
// 下发前把 UTF-8 path 转成客户端 ANSI(空/./\/\\ 为 ASCII,转换后不变),OnDriveList 直接用。
|
||
std::string ansiPath = ToAnsi(path, cp);
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_files", ansiPath))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_LIST_DRIVE;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for file list");
|
||
|
||
Json::Value drives(Json::arrayValue);
|
||
Json::Value files(Json::arrayValue);
|
||
int count = 0;
|
||
if (listDrives) {
|
||
drives = ParseDriveList(data, cp);
|
||
count = (int)drives.size();
|
||
} else {
|
||
files = ParseFileList(data, 500, cp); // 上限 500,避免 JSON 响应过大
|
||
count = (int)files.size();
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
if (listDrives) structuredContent["drives"] = drives;
|
||
else structuredContent["files"] = files;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = listDrives
|
||
? (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个驱动器。"))
|
||
: (std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个条目。"));
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_services(主连接下发 COMMAND_SERVICES,子连接一次性回传 TOKEN_SERVERLIST)
|
||
std::string BuildListServices(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
// 服务管理仅 Windows 客户端实现(EnumServicesStatus 等 A 接口);LNX/MAC 无 Windows
|
||
// 服务概念,提前返回避免 20s 超时等待。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
if (clientType == "LNX" || clientType == "MAC")
|
||
return BuildError(id, -32005, "list_services is only supported on Windows hosts");
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_services"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_SERVICES;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for service list");
|
||
|
||
// 服务名/路径/显示名来自 Windows A 接口 = 客户端 ANSI(GBK/936),按 clientType 判定。
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
Json::Value services = ParseServiceList(data, cp);
|
||
int count = (int)services.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["services"] = services;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string(count) + std::string(u8" 个服务。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:list_registry(主连接下发 COMMAND_REGEDIT → 子连接 TOKEN_REGEDIT → 下发
|
||
// COMMAND_REG_FIND → TOKEN_REG_PATH + TOKEN_REG_KEY 两包;path 空 = 列根键,无需查询客户端)
|
||
std::string BuildListRegistry(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
// 注册表仅 Windows 客户端实现(RegOpenKeyEx 等 A 接口);LNX/MAC 无注册表。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
if (clientType == "LNX" || clientType == "MAC")
|
||
return BuildError(id, -32005, "list_registry is only supported on Windows hosts");
|
||
|
||
std::string path = GetStringArg(args, "path");
|
||
BYTE rootToken = 0;
|
||
std::string relPath;
|
||
if (!ParseRegistryPath(path, rootToken, relPath)) {
|
||
if (path.empty())
|
||
return BuildRegistryRoots(id); // 空 path → 列根键
|
||
return BuildError(id, -32602,
|
||
"Invalid path: expected a root key like HKEY_LOCAL_MACHINE\\Software");
|
||
}
|
||
|
||
// 客户端 RegisterOperation 以 char KeyPath[MAX_PATH] 承载相对子键路径(SetPath→strcpy 无界),
|
||
// 超长会栈溢出。按 UTF-8 字节数做保守上限(GBK/936 字节数 ≤ UTF-8,故只查 UTF-8 即可)。
|
||
if (relPath.size() >= MAX_PATH)
|
||
return BuildError(id, -32602, "Registry path too long (exceeds MAX_PATH)");
|
||
|
||
// 注册表键路径为客户端 ANSI(GBK/936),下发前转好;rootToken 编码进 path[0] 供 OnRegeditReady 拆分。
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
std::string rootAndPath = std::string(1, (char)rootToken) + ToAnsi(relPath, cp);
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "list_registry", rootAndPath))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_REGEDIT;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> subkeys, values;
|
||
if (!mcp.WaitPendingRegistry(devId, subkeys, values, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for registry data");
|
||
|
||
Json::Value keysArr = ParseRegPath(subkeys, cp);
|
||
Json::Value valsArr = ParseRegKey(values, cp);
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["path"] = path;
|
||
structuredContent["keys"] = keysArr;
|
||
structuredContent["values"] = valsArr;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = std::string(u8"共 ") + std::to_string((int)keysArr.size()) + std::string(u8" 个子键、")
|
||
+ std::to_string((int)valsArr.size()) + std::string(u8" 个值。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// ========== P3:exec_command ==========
|
||
|
||
// exec_command 默认白名单:只读命令前缀(逗号分隔,前缀匹配,大小写不敏感)。
|
||
// 只放查询/只读类命令;写命令(del/copy/move/reg add/sc stop/...)一律不在列。
|
||
static const char* kDefaultCmdWhitelist =
|
||
"dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,"
|
||
"path,set,find,findstr,reg query,sc query,"
|
||
"ping,tracert,nslookup,getmac,driverquery,query,gpresult,"
|
||
"arp -a,route print,schtasks /query";
|
||
|
||
// 去首尾空白(空格/制表符/回车/换行)。
|
||
static std::string Trim(const std::string& s) {
|
||
size_t b = s.find_first_not_of(" \t\r\n");
|
||
if (b == std::string::npos) return "";
|
||
return s.substr(b, s.find_last_not_of(" \t\r\n") - b + 1);
|
||
}
|
||
|
||
// 去尾部空白(含尾部 CR/LF)。
|
||
static std::string TrimRight(const std::string& s) {
|
||
size_t e = s.find_last_not_of(" \t\r\n");
|
||
return (e == std::string::npos) ? "" : s.substr(0, e + 1);
|
||
}
|
||
|
||
// 命令白名单校验:命令(trim + ASCII 小写)以某白名单项为前缀,且其后字符为空/空格/制表符,
|
||
// 防止 "dirx" 前缀误配 "dir"。多词前缀(sc query / reg query)也按完整前缀匹配。
|
||
static bool IsCommandAllowed(const std::string& command, const std::string& whitelist) {
|
||
std::string c = ToLowerAscii(Trim(command));
|
||
if (c.empty()) return false;
|
||
size_t start = 0;
|
||
while (start < whitelist.size()) {
|
||
size_t comma = whitelist.find(',', start);
|
||
std::string item = Trim(whitelist.substr(
|
||
start, comma == std::string::npos ? std::string::npos : comma - start));
|
||
if (!item.empty()) {
|
||
std::string p = ToLowerAscii(item);
|
||
if (c.size() >= p.size() && c.compare(0, p.size(), p) == 0 &&
|
||
(c.size() == p.size() || c[p.size()] == ' ' || c[p.size()] == '\t')) {
|
||
return true;
|
||
}
|
||
}
|
||
if (comma == std::string::npos) break;
|
||
start = comma + 1;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 剥 ANSI 转义序列(CSI/OSC/单字节 ESC),避免终端彩色码污染返回文本。
|
||
static std::string StripAnsi(const std::string& s) {
|
||
std::string out;
|
||
out.reserve(s.size());
|
||
for (size_t i = 0; i < s.size(); ++i) {
|
||
if (s[i] != '\x1b') { out += s[i]; continue; }
|
||
++i; // 吞 ESC
|
||
if (i < s.size() && s[i] == '[') { // CSI ... 结尾 @–~
|
||
while (++i < s.size() && !(s[i] >= '@' && s[i] <= '~')) {}
|
||
} else if (i < s.size() && s[i] == ']') { // OSC ... 结尾 BEL/ESC
|
||
while (++i < s.size() && s[i] != '\x07' && s[i] != '\x1b') {}
|
||
}
|
||
// 其它单字节 ESC 序列吞掉当前字节即可(循环 ++i 会跳过它)
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// tools/call:exec_command(Windows 一次性远程命令:主连接 COMMAND_SHELL → 子连接终端 →
|
||
// 哨兵命令行 → 收集 stdout + exit_code → 关子链接)。安全门:只读默认 + 白名单 + 审计。
|
||
std::string BuildExecCommand(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
// 终端仅 Windows 客户端实现(ConPTY / 老 cmd 管道);LNX/MAC 远期再考虑。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
if (clientType == "LNX" || clientType == "MAC")
|
||
return BuildError(id, -32005, "exec_command is only supported on Windows hosts");
|
||
|
||
std::string command = Trim(GetStringArg(args, "command"));
|
||
if (command.empty())
|
||
return BuildError(id, -32602, "Missing required parameter: command");
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
|
||
// 安全门 1:只读模式直接拒绝(默认只读,exec_command 需显式关只读才可用)。
|
||
if (mcp.IsReadonly())
|
||
return BuildError(id, -32006, "exec_command is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
|
||
|
||
// 安全门 2:拒绝 shell 元字符,防止 "dir && del ..." 之类绕过白名单的注入。
|
||
if (command.find_first_of("&|<>^") != std::string::npos)
|
||
return BuildError(id, -32008, "Command contains forbidden shell characters: " + command);
|
||
|
||
// 安全门 3:命令白名单前缀校验。
|
||
std::string whitelist = mcp.GetCmdWhitelist();
|
||
if (whitelist.empty()) whitelist = kDefaultCmdWhitelist;
|
||
if (!IsCommandAllowed(command, whitelist))
|
||
return BuildError(id, -32007, "Command not allowed by whitelist: " + command);
|
||
|
||
int timeoutMs = kMcpToolTimeoutMs;
|
||
std::string t = GetStringArg(args, "timeout_ms");
|
||
if (!t.empty() && IsDigits(t)) {
|
||
int v = atoi(t.c_str());
|
||
if (v > 0 && v <= 600000) timeoutMs = v;
|
||
}
|
||
|
||
std::string nonce = GenerateRandomToken().substr(0, 8);
|
||
|
||
if (!mcp.BeginTermPending(devId, command, nonce))
|
||
return BuildError(id, -32003, "Device busy: a terminal session is already active for this host");
|
||
|
||
// 主连接下发 COMMAND_SHELL,客户端建立 shell 子连接并回 TOKEN_*_START。
|
||
BYTE cmd = COMMAND_SHELL;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearTermPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
context* subCtx = nullptr;
|
||
bool isPty = false;
|
||
if (!mcp.WaitTerminalReady(devId, subCtx, isPty, timeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for shell to start");
|
||
|
||
// 编码:ConPTY(UTF-8) / 老 cmd 管道(GBK)。哨兵为纯 ASCII,两种编码下字节一致。
|
||
UINT cp = isPty ? CP_UTF8 : 936;
|
||
// 哨兵命令行:@echo off 仅 ConPTY 需要(抑制回显噪声);老 ShellManager 已自行跳过回显,
|
||
// 加 @echo off 反而破坏其回显跳过逻辑。&&/|| 取命令真实退出码(%errorlevel% 在复合句中
|
||
// 是解析期展开、已过期,故用控制操作符)。哨兵为随机串,命令输出不可能恰好包含。
|
||
std::string line;
|
||
if (isPty) line += "@echo off & ";
|
||
line += command;
|
||
line += " 2>&1 && echo __MCP_DONE_" + nonce + "__0 || echo __MCP_DONE_" + nonce + "__1";
|
||
std::string wireLine = ToAnsi(line, cp) + "\r\n";
|
||
subCtx->Send2Client((BYTE*)wireLine.data(), (ULONG)wireLine.size());
|
||
|
||
std::vector<BYTE> raw;
|
||
int exitCode = -1;
|
||
bool closed = false;
|
||
if (!mcp.WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)) {
|
||
subCtx->CancelIO();
|
||
return BuildError(id, -32001, "Timeout waiting for command output");
|
||
}
|
||
subCtx->CancelIO(); // 关子链接,结束 shell 进程
|
||
|
||
// 清洗:raw → UTF-8 → 剥 ANSI → CRLF 归一。raw 已截断到哨兵前。
|
||
std::string stdoutStr;
|
||
if (!raw.empty()) {
|
||
std::string rawStr((const char*)raw.data(), raw.size());
|
||
stdoutStr = StripAnsi(ToUtf8(rawStr.c_str(), cp));
|
||
size_t p = 0;
|
||
while ((p = stdoutStr.find("\r\n", p)) != std::string::npos)
|
||
stdoutStr.erase(p, 1);
|
||
stdoutStr = TrimRight(stdoutStr);
|
||
}
|
||
|
||
// 审计:命令执行落服务端消息/审计日志(不可关闭)。
|
||
if (parent) {
|
||
std::string text = "host " + std::to_string(devId) + " exec: " + command;
|
||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||
(LPARAM)new CString(_TR("MCP命令执行"))); // 标题走语言映射(GBK,随语言切换)
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["stdout"] = stdoutStr;
|
||
structuredContent["exit_code"] = exitCode;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = stdoutStr.empty() ? std::string(u8"(无输出)") : stdoutStr;
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_client_log(主连接下发 COMMAND_QUERY_LOG,子连接回传 TOKEN_REPORT_LOG)
|
||
std::string BuildGetClientLog(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||
uint64_t devId = 0;
|
||
std::string err;
|
||
if (!ParseHostIdArg(args, devId, err))
|
||
return BuildError(id, -32602, err);
|
||
|
||
context* ctx = FindMainContext(parent, devId);
|
||
if (!ctx)
|
||
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
|
||
|
||
CMcpServer& mcp = CMcpServer::Instance();
|
||
if (!mcp.BeginPending(devId, "get_client_log"))
|
||
return BuildError(id, -32003, "Device busy: another request is pending for this host");
|
||
|
||
BYTE cmd = COMMAND_QUERY_LOG;
|
||
if (!ctx->Send2Client(&cmd, 1)) {
|
||
mcp.ClearPending(devId);
|
||
return BuildError(id, -32004, "Failed to send command to host");
|
||
}
|
||
|
||
std::vector<BYTE> data;
|
||
if (!mcp.WaitPending(devId, data, kMcpToolTimeoutMs))
|
||
return BuildError(id, -32001, "Timeout waiting for client log");
|
||
|
||
// data[0]=token,其后为 Logger 内存 ring buffer 的日志文本(无 '\0' 终止)。
|
||
// 客户端在子连接建立时 m_sentIdx=0 返回全量,随后每 3s 推增量;MessageHandle 在
|
||
// 取走首条全量后立即 CancelIO 关子链接,增量不再到达,故此处即为完整快照。
|
||
// 日志文本为客户端 ANSI(Windows 走 vsnprintf A 版),按 clientType 判定编码。
|
||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
|
||
|
||
std::string log;
|
||
if (data.size() > 1) {
|
||
std::string raw((const char*)data.data() + 1, data.size() - 1);
|
||
log = ToUtf8(raw.c_str(), cp);
|
||
}
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["log"] = log;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = log.empty() ? std::string(u8"客户端暂无内存日志。")
|
||
: std::string(u8"客户端运行日志快照如下。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call:get_audit_log(服务端本地消息日志,读取 UI 日志列表的线程安全镜像)
|
||
std::string BuildGetAuditLog(const Json::Value& id, CMy2015RemoteDlg* parent) {
|
||
Json::Value entries(Json::arrayValue);
|
||
if (parent) {
|
||
EnterCriticalSection(&parent->m_cs);
|
||
// m_MessageLog 新在前、旧在后(与界面一致),直接顺序输出。
|
||
for (const auto& e : parent->m_MessageLog) {
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = ToUtf8(e.type.c_str(), 936);
|
||
item["time"] = ToUtf8(e.time.c_str(), 936);
|
||
item["msg"] = ToUtf8(e.msg.c_str(), 936);
|
||
entries.append(item);
|
||
}
|
||
LeaveCriticalSection(&parent->m_cs);
|
||
}
|
||
|
||
int count = (int)entries.size();
|
||
|
||
Json::Value result(Json::objectValue);
|
||
Json::Value structuredContent(Json::objectValue);
|
||
structuredContent["entries"] = entries;
|
||
result["structuredContent"] = structuredContent;
|
||
|
||
Json::Value content(Json::arrayValue);
|
||
Json::Value item(Json::objectValue);
|
||
item["type"] = "text";
|
||
item["text"] = count > 0 ? std::string(u8"共 ") + std::to_string(count) + std::string(u8" 条日志。")
|
||
: std::string(u8"当前无日志。");
|
||
content.append(item);
|
||
result["content"] = content;
|
||
result["isError"] = false;
|
||
|
||
return BuildResult(id, result);
|
||
}
|
||
|
||
// tools/call 分派
|
||
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||
const Json::Value& id = root["id"];
|
||
Json::Value params = root.isMember("params") ? root["params"] : Json::Value(Json::objectValue);
|
||
|
||
std::string toolName;
|
||
if (params.isObject() && params.isMember("name") && params["name"].isString()) {
|
||
toolName = params["name"].asString();
|
||
}
|
||
|
||
const Json::Value args = GetCallArguments(params);
|
||
|
||
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
|
||
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
|
||
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
|
||
if (toolName == "list_processes") return BuildListProcesses(id, args, parent);
|
||
if (toolName == "list_windows") return BuildListWindows(id, args, parent);
|
||
if (toolName == "get_activity_history") return BuildGetActivityHistory(id, args, parent);
|
||
if (toolName == "get_screenshot") return BuildGetScreenshot(id, args, parent);
|
||
if (toolName == "list_files") return BuildListFiles(id, args, parent);
|
||
if (toolName == "list_services") return BuildListServices(id, args, parent);
|
||
if (toolName == "get_client_log") return BuildGetClientLog(id, args, parent);
|
||
if (toolName == "get_audit_log") return BuildGetAuditLog(id, parent);
|
||
if (toolName == "list_registry") return BuildListRegistry(id, args, parent);
|
||
if (toolName == "exec_command") return BuildExecCommand(id, args, parent);
|
||
|
||
return BuildError(id, -32602,
|
||
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// ===== P3:exec_command 哨兵检测与终端会话 =====
|
||
|
||
// 在原始 shell 输出字节流中定位哨兵(纯 ASCII,编码无关)。真实哨兵由 echo 单独输出、独占
|
||
// 一行(前面是 \n 或缓冲区开头);而 ConPTY 会把整条命令行回显进输出流,哨兵字样嵌在
|
||
// "&& echo ... || echo ..." 里、前面是空格。据此从后往前找「行首命中」,排除回显行误判:
|
||
// 否则回显包先于命令输出单独到达时,rfind 会命中回显里的 __1 提前结束、截断真实输出。
|
||
// 老 ShellManager 已自行跳过回显,输出里只有真实哨兵(仍为行首)。命中 __0 → exit 0;__1 → exit 1。
|
||
static bool FindSentinel(const std::vector<BYTE>& buf, const std::string& nonce,
|
||
size_t& pos, int& exitCode) {
|
||
std::string marker = "__MCP_DONE_" + nonce + "__";
|
||
if (marker.size() + 1 > buf.size()) return false;
|
||
std::string s((const char*)buf.data(), buf.size());
|
||
size_t from = std::string::npos;
|
||
while (true) {
|
||
size_t p = s.rfind(marker, from);
|
||
if (p == std::string::npos) return false;
|
||
bool lineStart = (p == 0) || (s[p - 1] == '\n');
|
||
size_t digitPos = p + marker.size();
|
||
if (lineStart && digitPos < s.size() &&
|
||
(s[digitPos] == '0' || s[digitPos] == '1')) {
|
||
exitCode = (s[digitPos] == '0') ? 0 : 1;
|
||
pos = p;
|
||
return true;
|
||
}
|
||
if (p == 0) return false;
|
||
from = p - 1;
|
||
}
|
||
}
|
||
|
||
bool CMcpServer::IsTermPending(uint64_t device_id) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermSessions.find(device_id);
|
||
return it != m_TermSessions.end() && !it->second.started;
|
||
}
|
||
|
||
void CMcpServer::RegisterTerminalContext(uint64_t device_id, context* subCtx, bool isPty) {
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermSessions.find(device_id);
|
||
if (it == m_TermSessions.end() || it->second.started) return; // 没有等它的会话
|
||
TermSession& s = it->second;
|
||
s.started = true;
|
||
s.subCtx = subCtx;
|
||
s.isPty = isPty;
|
||
m_TermContextToDevice[subCtx] = device_id;
|
||
}
|
||
|
||
// 关键步骤:告知客户端「启动 shell 输出回流」。客户端读线程靠 COMMAND_NEXT 才启动,
|
||
// 漏发会导致 shell 在跑但输出永不送回。PTY 还要先告知初始 80x24,否则 TUI 尺寸错乱。
|
||
if (isPty) {
|
||
BYTE resizeBuf[5];
|
||
resizeBuf[0] = CMD_TERMINAL_RESIZE;
|
||
*(short*)(resizeBuf + 1) = (short)80;
|
||
*(short*)(resizeBuf + 3) = (short)24;
|
||
subCtx->Send2Client(resizeBuf, 5);
|
||
}
|
||
BYTE startCmd = COMMAND_NEXT;
|
||
subCtx->Send2Client(&startCmd, 1);
|
||
|
||
m_TermCv.notify_all();
|
||
}
|
||
|
||
bool CMcpServer::IsTerminalContext(context* subCtx) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
return m_TermContextToDevice.find(subCtx) != m_TermContextToDevice.end();
|
||
}
|
||
|
||
void CMcpServer::OnTerminalData(context* subCtx, const BYTE* data, ULONG len) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermContextToDevice.find(subCtx);
|
||
if (it == m_TermContextToDevice.end()) return;
|
||
auto sit = m_TermSessions.find(it->second);
|
||
if (sit == m_TermSessions.end()) return;
|
||
TermSession& s = sit->second;
|
||
if (s.done || s.closed) return; // 已结束,忽略迟到数据
|
||
s.data.insert(s.data.end(), data, data + len);
|
||
if (FindSentinel(s.data, s.nonce, s.sentPos, s.exitCode)) {
|
||
s.done = true;
|
||
m_TermCv.notify_all();
|
||
}
|
||
}
|
||
|
||
void CMcpServer::OnTerminalClosed(context* subCtx) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermContextToDevice.find(subCtx);
|
||
if (it == m_TermContextToDevice.end()) return;
|
||
auto sit = m_TermSessions.find(it->second);
|
||
if (sit == m_TermSessions.end()) return;
|
||
sit->second.closed = true;
|
||
m_TermCv.notify_all();
|
||
}
|
||
|
||
bool CMcpServer::BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
if (m_TermSessions.find(device_id) != m_TermSessions.end()) return false; // 已有会话
|
||
TermSession s;
|
||
s.command = command;
|
||
s.nonce = nonce;
|
||
m_TermSessions[device_id] = std::move(s);
|
||
return true;
|
||
}
|
||
|
||
bool CMcpServer::WaitTerminalReady(uint64_t device_id, context*& subCtx, bool& isPty, int timeoutMs) {
|
||
std::unique_lock<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermSessions.find(device_id);
|
||
if (it == m_TermSessions.end()) return false;
|
||
|
||
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||
[&] { return it->second.started; });
|
||
if (!signaled) {
|
||
m_TermSessions.erase(it); // 超时 → 清理
|
||
return false;
|
||
}
|
||
subCtx = it->second.subCtx;
|
||
isPty = it->second.isPty;
|
||
return true;
|
||
}
|
||
|
||
bool CMcpServer::WaitTerminalDone(uint64_t device_id, std::vector<BYTE>& out,
|
||
int& exitCode, bool& closed, int timeoutMs) {
|
||
std::unique_lock<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermSessions.find(device_id);
|
||
if (it == m_TermSessions.end()) return false;
|
||
|
||
bool signaled = m_TermCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||
[&] { return it->second.done || it->second.closed; });
|
||
|
||
if (!signaled) { // 超时 → 清理(含路由表)
|
||
m_TermContextToDevice.erase(it->second.subCtx);
|
||
m_TermSessions.erase(it);
|
||
return false;
|
||
}
|
||
|
||
TermSession s = it->second; // 拷贝出,避免擦除后悬空
|
||
m_TermContextToDevice.erase(s.subCtx);
|
||
m_TermSessions.erase(it);
|
||
|
||
closed = s.closed;
|
||
exitCode = s.exitCode;
|
||
if (s.done && s.sentPos <= s.data.size())
|
||
out.assign(s.data.begin(), s.data.begin() + s.sentPos); // 截断到哨兵前
|
||
else
|
||
out = s.data; // 进程退出无哨兵:返回已收集的原始输出
|
||
return true;
|
||
}
|
||
|
||
void CMcpServer::ClearTermPending(uint64_t device_id) {
|
||
std::lock_guard<std::mutex> lk(m_TermMutex);
|
||
auto it = m_TermSessions.find(device_id);
|
||
if (it != m_TermSessions.end()) m_TermSessions.erase(it);
|
||
}
|
||
|
||
//////////////////////////////////////////////////////////////////////////
|
||
// CMcpServer Implementation
|
||
//////////////////////////////////////////////////////////////////////////
|
||
|
||
CMcpServer& CMcpServer::Instance() {
|
||
static CMcpServer instance;
|
||
return instance;
|
||
}
|
||
|
||
CMcpServer::CMcpServer() {
|
||
m_server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
|
||
HandleMcp(req, res);
|
||
});
|
||
}
|
||
|
||
CMcpServer::~CMcpServer() {
|
||
Stop(); // 兜底:确保监听线程 join,避免 std::thread 析构触发 terminate
|
||
}
|
||
|
||
bool CMcpServer::Start(const std::string& bind, int port) {
|
||
if (m_running.load()) return true; // 已在运行
|
||
|
||
m_thread = std::thread([this, bind, port]() {
|
||
m_server.listen(bind, port);
|
||
});
|
||
|
||
// 给 listen 一点时间绑定端口;httplib::Server::is_running() 在 listen 内部置位。
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||
m_running.store(m_server.is_running());
|
||
return m_running.load();
|
||
}
|
||
|
||
void CMcpServer::Stop() {
|
||
m_server.stop();
|
||
if (m_thread.joinable()) {
|
||
m_thread.join();
|
||
}
|
||
m_running.store(false);
|
||
}
|
||
|
||
void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res) {
|
||
res.set_header("Content-Type", "application/json");
|
||
|
||
// 静态 token 校验:Authorization: Bearer <token>(Start 前经 SetToken 保证非空)
|
||
if (req.get_header_value("Authorization") != ("Bearer " + m_token)) {
|
||
res.status = 401;
|
||
res.set_content(BuildError(Json::nullValue, -32000, "Unauthorized"), "application/json");
|
||
return;
|
||
}
|
||
|
||
// 解析 JSON-RPC 请求体
|
||
Json::Value root;
|
||
Json::CharReaderBuilder rbuilder;
|
||
std::string errs;
|
||
std::istringstream iss(req.body);
|
||
if (!Json::parseFromStream(rbuilder, iss, &root, &errs) || !root.isObject()) {
|
||
res.set_content(BuildError(Json::nullValue, -32700, "Parse error"), "application/json");
|
||
return;
|
||
}
|
||
|
||
// 通知(无 id)→ 不返回 JSON-RPC 响应(如 notifications/initialized)
|
||
if (!root.isMember("id")) {
|
||
res.status = 202;
|
||
res.set_content("", "application/json");
|
||
return;
|
||
}
|
||
|
||
// 结构校验:缺 method
|
||
if (!root.isMember("method") || !root["method"].isString()) {
|
||
res.set_content(BuildError(root["id"], -32600, "Invalid Request"), "application/json");
|
||
return;
|
||
}
|
||
|
||
std::string method = root["method"].asString();
|
||
|
||
if (method == "initialize") {
|
||
res.set_content(BuildInitializeResult(root["id"]), "application/json");
|
||
return;
|
||
}
|
||
if (method == "ping") {
|
||
res.set_content(BuildPingResult(root["id"]), "application/json");
|
||
return;
|
||
}
|
||
if (method == "tools/list") {
|
||
res.set_content(BuildToolsListResult(root["id"]), "application/json");
|
||
return;
|
||
}
|
||
if (method == "tools/call") {
|
||
res.set_content(BuildToolsCall(root, m_parent), "application/json");
|
||
return;
|
||
}
|
||
|
||
// 未实现的方法
|
||
res.set_content(BuildError(root["id"], -32601, "Method not found"), "application/json");
|
||
}
|
||
|
||
// ===== P2b 挂起请求注册表实现 =====
|
||
|
||
bool CMcpServer::IsPending(uint64_t device_id) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
return m_Pending.find(device_id) != m_Pending.end();
|
||
}
|
||
|
||
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃
|
||
it->second.data.assign(data, data + len);
|
||
it->second.done = true;
|
||
m_PendingCv.notify_one();
|
||
}
|
||
|
||
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||
PendingRequest r;
|
||
r.tool = tool;
|
||
m_Pending[device_id] = std::move(r);
|
||
return true;
|
||
}
|
||
|
||
bool CMcpServer::WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
|
||
std::unique_lock<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return false;
|
||
|
||
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||
[&] { return it->second.done; });
|
||
if (!signaled || it->second.data.empty()) {
|
||
m_Pending.erase(it); // 超时/空数据 → 清理
|
||
return false;
|
||
}
|
||
out = std::move(it->second.data);
|
||
m_Pending.erase(it);
|
||
return true;
|
||
}
|
||
|
||
void CMcpServer::ClearPending(uint64_t device_id) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
m_Pending.erase(device_id);
|
||
}
|
||
|
||
// ===== P2c:list_files / get_screenshot 扩展 =====
|
||
|
||
bool CMcpServer::BeginPending(uint64_t device_id, const std::string& tool, const std::string& path) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||
PendingRequest r;
|
||
r.tool = tool;
|
||
r.path = path;
|
||
m_Pending[device_id] = std::move(r);
|
||
return true;
|
||
}
|
||
|
||
uint16_t CMcpServer::NextPreviewReqId() {
|
||
uint16_t v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed);
|
||
if (v == 0) v = m_PreviewReqId.fetch_add(1, std::memory_order_relaxed); // 跳过 0
|
||
return v;
|
||
}
|
||
|
||
void CMcpServer::SetPendingReqId(uint64_t device_id, uint16_t reqId) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it != m_Pending.end()) it->second.expectedReqId = reqId;
|
||
}
|
||
|
||
bool CMcpServer::TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return false; // 已超时清理 → 迟到数据,回落 MFC
|
||
if (it->second.tool != "get_screenshot") return false;
|
||
if (it->second.expectedReqId == 0 || it->second.expectedReqId != reqId) return false; // 过期/他途响应
|
||
it->second.data.assign(data, data + len);
|
||
it->second.done = true;
|
||
m_PendingCv.notify_one();
|
||
return true;
|
||
}
|
||
|
||
bool CMcpServer::OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len) {
|
||
std::string path;
|
||
bool listDrives = true;
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return true; // 已超时清理 → 调用方 CancelIO 收尾
|
||
path = it->second.path;
|
||
listDrives = path.empty() || path == "." || path == "/" || path == "\\";
|
||
if (listDrives) {
|
||
it->second.data.assign(buf, buf + len);
|
||
it->second.done = true;
|
||
m_PendingCv.notify_one();
|
||
return true; // 只列盘 → 调用方用完即关
|
||
}
|
||
}
|
||
|
||
// 列目录:锁外下发 COMMAND_LIST_FILES + path,子链接保持,等 TOKEN_FILE_LIST。
|
||
// path 已在 BuildListFiles 按客户端 ANSI 转好(OnDriveList 无需再转),结尾 '\0'
|
||
// 与 FileManagerDlg 的 PacketSize=len+2 一致。
|
||
std::vector<BYTE> pkt;
|
||
pkt.reserve(1 + path.size() + 1);
|
||
pkt.push_back((BYTE)COMMAND_LIST_FILES);
|
||
pkt.insert(pkt.end(), path.begin(), path.end());
|
||
pkt.push_back(0);
|
||
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
|
||
return false; // 继续等 TOKEN_FILE_LIST,调用方不 CancelIO
|
||
}
|
||
|
||
// ===== P3:list_registry 扩展 =====
|
||
|
||
bool CMcpServer::OnRegeditReady(uint64_t device_id, context* subCtx) {
|
||
std::string rootAndPath;
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return false; // 无挂起 → 回落 MFC
|
||
if (it->second.tool != "list_registry") return false;
|
||
rootAndPath = it->second.path; // [rootToken:1][相对子键路径...]
|
||
}
|
||
if (rootAndPath.empty()) return false; // 不应发生:list_registry 必有 rootToken
|
||
|
||
BYTE rootToken = (BYTE)rootAndPath[0];
|
||
std::string relPath = rootAndPath.substr(1);
|
||
|
||
// 下发 COMMAND_REG_FIND:布局与 RegisterDlg::OnTvnSelchangedTree 一致
|
||
// [COMMAND_REG_FIND][rootToken][relPath...]['\0']。子链接保持,等 TOKEN_REG_PATH + KEY。
|
||
std::vector<BYTE> pkt;
|
||
pkt.reserve(2 + relPath.size() + 1);
|
||
pkt.push_back((BYTE)COMMAND_REG_FIND);
|
||
pkt.push_back(rootToken);
|
||
pkt.insert(pkt.end(), relPath.begin(), relPath.end());
|
||
pkt.push_back(0);
|
||
subCtx->Send2Client(pkt.data(), (ULONG)pkt.size());
|
||
return true; // 接管子链接,不打开 MFC 对话框
|
||
}
|
||
|
||
void CMcpServer::TakeRegPath(uint64_t device_id, const BYTE* data, ULONG len) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据丢弃
|
||
if (it->second.tool != "list_registry") return;
|
||
it->second.regPath.assign(data, data + len);
|
||
it->second.pathDone = true;
|
||
if (it->second.keyDone) {
|
||
it->second.done = true;
|
||
m_PendingCv.notify_one();
|
||
}
|
||
}
|
||
|
||
void CMcpServer::TakeRegKey(uint64_t device_id, const BYTE* data, ULONG len) {
|
||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return;
|
||
if (it->second.tool != "list_registry") return;
|
||
it->second.regKey.assign(data, data + len);
|
||
it->second.keyDone = true;
|
||
if (it->second.pathDone) {
|
||
it->second.done = true;
|
||
m_PendingCv.notify_one();
|
||
}
|
||
}
|
||
|
||
bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subkeys,
|
||
std::vector<BYTE>& values, int timeoutMs) {
|
||
std::unique_lock<std::mutex> lk(m_PendingMutex);
|
||
auto it = m_Pending.find(device_id);
|
||
if (it == m_Pending.end()) return false;
|
||
|
||
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||
[&] { return it->second.done; });
|
||
if (!signaled) {
|
||
m_Pending.erase(it); // 超时 → 清理
|
||
return false;
|
||
}
|
||
subkeys = std::move(it->second.regPath);
|
||
values = std::move(it->second.regKey);
|
||
m_Pending.erase(it);
|
||
return true;
|
||
}
|
||
|
||
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
|
||
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
|
||
extern "C" int __cdecl rand_s(unsigned int* randomValue);
|
||
|
||
std::string GenerateRandomToken() {
|
||
static const char hex[] = "0123456789abcdef";
|
||
std::string out;
|
||
out.reserve(32);
|
||
for (int i = 0; i < 16; ++i) {
|
||
unsigned int v = 0;
|
||
if (rand_s(&v) != 0) {
|
||
// rand_s 失败(罕见):退化为时间 + 地址熵,保证仍返回非空 token。
|
||
v = (unsigned int)(GetTickCount() ^ (ULONG_PTR)&out);
|
||
}
|
||
out.push_back(hex[(v >> 4) & 0xF]);
|
||
out.push_back(hex[v & 0xF]);
|
||
}
|
||
return out;
|
||
}
|