// McpMachine.cpp : MCP「连接管理」与「主机管理」工具族 // // 连接管理:主连接 COMMAND_CONNECTION(253) → 客户端 CConnectionsManager 子连接回 // TOKEN_CONNECTION_LIST(255)。包格式 [255][u16 LE count][ConnectionInfo × count], // ConnectionInfo 为 110 字节 packed 结构。强制断开 = 回发 [254][原始 110 字节]。 // 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 CMachineManager 子连接回 // TOKEN_SYSINFOLIST(61) → 之后在同一子连接上发 1 字节 tab(0..9),回 // TOKEN_MACHINE_*(31..42)。动作 10..30 由客户端处理,多数动作会自动回一份 // 刷新后的列表(服务 = 40,任务 = 41),进程冻结/解冻与窗口操作不回包。 // // 会话状态(m_Pending 中的 Machine / ConnMgr 条目)全部由 CMcpServer 持有; // 本文件只负责工具 schema、参数校验、组包,以及响应解析。协议与设计见 // docs/Mcp_Machine_Design.md。 #include "stdafx.h" #include "McpMachine.h" #include "McpServer.h" #include "McpInternal.h" #include "context.h" #include "2015RemoteDlg.h" #include "LangManager.h" #include #include #include #include #include #include #include #include using namespace McpInternal; namespace { // ========== 错误码(与 McpServer.cpp 保持一致,另加 -32009) ========== const int kErrGeneric = -32000; const int kErrTimeout = -32001; const int kErrOffline = -32002; const int kErrBusy = -32003; const int kErrSend = -32004; const int kErrUnsupported = -32005; const int kErrDisabled = -32006; const int kErrNotFound = -32009; // 目标在最新快照中不存在(PID/HWND/服务/任务/连接) // ========== 超时(ms) ========== const int kReadyTimeoutMs = 5000; // 主连接发命令 → 子连接回就绪包 const int kListTimeoutMs = 12000; // tab 请求 → 列表响应(服务/软件枚举较慢) const int kActionTimeoutMs = 12000; // 动作 → 客户端自动回的刷新列表 // 整个会话(含多步动作)的截止时间。restart 需要 prime + stop + start 三步, // 每步最多 12s,故留 60s 余量;单步等待再按剩余时间收敛。 const int kSessionDeadlineMs = 60000; // 单个包的上限:正常列表远小于此;超过即视为异常,置 clientError 并断开子连接。 const size_t kMaxPacketBytes = 8u * 1024u * 1024u; const size_t kConnRecordSize = sizeof(ConnectionInfo); static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo layout changed; update the parser"); static_assert(sizeof(WINDOWSINFO) == 1044, "WINDOWSINFO layout changed; update the parser"); // ========== 小工具 ========== std::string Trim(const std::string& s) { size_t b = 0, e = s.size(); while (b < e && (s[b] == ' ' || s[b] == '\t')) ++b; while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t')) --e; return s.substr(b, e - b); } bool MatchCI(const std::string& value, const std::string& needle) { return needle.empty() || ContainsCI(value, needle); } bool EqualsCI(const std::string& a, const std::string& b) { return a.size() == b.size() && _stricmp(a.c_str(), b.c_str()) == 0; } bool GetBoolArg(const Json::Value& args, const char* key, bool def) { if (!args.isMember(key)) return def; const Json::Value& v = args[key]; if (v.isBool()) return v.asBool(); if (v.isNumeric()) return v.asInt() != 0; if (v.isString()) { std::string s = ToLowerAscii(v.asString()); return s == "1" || s == "true" || s == "yes"; } return def; } // 可选整数:缺省不算错;类型/范围错写 errJson。 bool GetOptIntArg(const Json::Value& id, const Json::Value& args, const char* key, int minV, int maxV, int& out, std::string& errJson) { if (!args.isMember(key)) return true; int v = 0; if (!GetIntArg(args, key, v)) { errJson = BuildError(id, -32602, std::string("Invalid parameter '") + key + "': expected an integer"); return false; } if (v < minV || v > maxV) { errJson = BuildError(id, -32602, std::string("Parameter '") + key + "' out of range [" + std::to_string(minV) + ", " + std::to_string(maxV) + "]"); return false; } out = v; return true; } std::string OptStr(const Json::Value& args, const char* key) { return Trim(GetStringArg(args, key)); } // 严格 UTF-8 → 目标代码页:任一字符无法表示即失败(不用 '?' 顶替)。 bool Utf8ToCpExact(const std::string& utf8, UINT cp, std::string& out) { out.clear(); if (utf8.empty()) return true; int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0); if (wlen <= 0) return false; std::wstring w(wlen, L'\0'); if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), &w[0], wlen) != wlen) return false; BOOL usedDefault = FALSE; int alen = WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, NULL, 0, NULL, &usedDefault); if (alen <= 0 || usedDefault) return false; out.assign(alen, '\0'); usedDefault = FALSE; if (WideCharToMultiByte(cp, WC_NO_BEST_FIT_CHARS, w.c_str(), wlen, &out[0], alen, NULL, &usedDefault) != alen || usedDefault) { out.clear(); return false; } return true; } bool IsValidUtf8(const char* s, size_t n) { if (!s || n == 0) return true; int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0); return wlen > 0; } bool IsWindowsClient(context* ctx) { if (!ctx) return false; CString t = ctx->GetAdditionalData(RES_CLIENT_TYPE); return !(t == "LNX" || t == "MAC" || t == "APK"); } std::string EndpointAddr(const std::string& ep) { size_t c = ep.rfind(':'); return c == std::string::npos ? ep : ep.substr(0, c); } int EndpointPort(const std::string& ep) { size_t c = ep.rfind(':'); if (c == std::string::npos) return 0; std::string p = ep.substr(c + 1); return IsDigits(p) ? atoi(p.c_str()) : 0; } const char* TcpStateName(DWORD state) { switch (state) { case 1: return "CLOSED"; case 2: return "LISTEN"; case 3: return "SYN_SENT"; case 4: return "SYN_RCVD"; case 5: return "ESTABLISHED"; case 6: return "FIN_WAIT1"; case 7: return "FIN_WAIT2"; case 8: return "CLOSE_WAIT"; case 9: return "CLOSING"; case 10: return "LAST_ACK"; case 11: return "TIME_WAIT"; case 12: return "DELETE_TCB"; default: return ""; } } const char* PriorityName(DWORD pri) { switch (pri) { case 0x00000040: return "Idle"; case 0x00004000: return "BelowNormal"; case 0x00000020: return "Normal"; case 0x00008000: return "AboveNormal"; case 0x00000080: return "High"; case 0x00000100: return "Realtime"; default: return ""; } } // OLE Automation DATE → "YYYY-MM-DD hh:mm:ss"(与 sys/MachineDlg.cpp:oleTime2Str 完全一致) std::string OleDateToStr(double t) { if (t == 0.0) return ""; time_t tt = (time_t)(t * 24.0 * 3600.0 - 2209190400.0); struct tm tm1; if (localtime_s(&tm1, &tt) != 0) return ""; char buf[32] = { 0 }; sprintf_s(buf, "%04d-%02d-%02d %02d:%02d:%02d", tm1.tm_year + 1900, tm1.tm_mon + 1, tm1.tm_mday, tm1.tm_hour, tm1.tm_min, tm1.tm_sec); return buf; } // ========== schema 辅助 ========== Json::Value PropStr(const char* desc = nullptr) { Json::Value p(Json::objectValue); p["type"] = "string"; if (desc) p["description"] = desc; return p; } Json::Value PropInt(const char* desc = nullptr) { Json::Value p(Json::objectValue); p["type"] = "integer"; if (desc) p["description"] = desc; return p; } Json::Value PropBool(const char* desc = nullptr) { Json::Value p(Json::objectValue); p["type"] = "boolean"; if (desc) p["description"] = desc; return p; } Json::Value PropStrEnum(const char* desc, std::initializer_list values) { Json::Value p = PropStr(desc); Json::Value enums(Json::arrayValue); for (const char* v : values) enums.append(v); p["enum"] = enums; return p; } Json::Value MakeSchema(const Json::Value& props, std::initializer_list required) { Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; if (required.size() > 0) { Json::Value req(Json::arrayValue); for (const char* r : required) req.append(r); schema["required"] = req; } return schema; } // 列表类 outputSchema:{ : [...], total, totalAll, offset, limit, hasMore } Json::Value MakeListOutputSchema(const char* arrayKey, const Json::Value& itemProps) { Json::Value props(Json::objectValue); Json::Value arr(Json::objectValue); arr["type"] = "array"; Json::Value items(Json::objectValue); items["type"] = "object"; items["properties"] = itemProps; arr["items"] = items; props[arrayKey] = arr; props["total"] = PropInt(u8"过滤后的条目数"); props["totalAll"] = PropInt(u8"未过滤的条目总数"); props["offset"] = PropInt(); props["limit"] = PropInt(); props["hasMore"] = PropBool(); Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value req(Json::arrayValue); req.append(arrayKey); schema["required"] = req; return schema; } Json::Value HostIdProp() { return PropStr(u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段"); } // 所有读工具的公共分页参数 void AddPagingProps(Json::Value& props) { props["offset"] = PropInt(u8"分页起点,默认 0"); props["limit"] = PropInt(u8"本次最多返回条数,默认 100,上限 1000"); } Json::Value ActionOutputSchema() { Json::Value props(Json::objectValue); props["ok"] = PropBool(u8"动作是否达到预期(false 表示未生效/无法确认,不会伪报成功)"); props["action"] = PropStr(); props["target"] = PropStr(); props["verified"] = PropBool(u8"是否已用动作后的最新快照复核"); props["changed"] = PropBool(u8"是否真的改变了状态(如服务本来就是目标状态时为 false)"); props["state"] = PropStr(u8"复核时观测到的状态"); props["reason"] = PropStr(u8"ok=false 时的原因"); props["message"] = PropStr(u8"人类可读的说明"); Json::Value schema(Json::objectValue); schema["type"] = "object"; schema["properties"] = props; Json::Value req(Json::arrayValue); req.append("ok"); schema["required"] = req; return schema; } std::string BuildStructuredResult(const Json::Value& id, const Json::Value& sc, const std::string& text) { Json::Value result(Json::objectValue); result["structuredContent"] = sc; Json::Value content(Json::arrayValue); Json::Value item(Json::objectValue); item["type"] = "text"; item["text"] = text; content.append(item); result["content"] = content; result["isError"] = false; return BuildResult(id, result); } std::string BuildListResult(const Json::Value& id, const char* arrayKey, const Json::Value& page, int total, int totalAll, int offset, int limit, bool hasMore, const std::string& text) { Json::Value sc(Json::objectValue); sc[arrayKey] = page; sc["total"] = total; sc["totalAll"] = totalAll; sc["offset"] = offset; sc["limit"] = limit; sc["hasMore"] = hasMore; return BuildStructuredResult(id, sc, text); } std::string BuildActionResult(const Json::Value& id, bool ok, bool verified, bool changed, const std::string& action, const std::string& target, const std::string& state, const std::string& reason, const std::string& message) { Json::Value sc(Json::objectValue); sc["ok"] = ok; sc["action"] = action; sc["target"] = target; sc["verified"] = verified; sc["changed"] = changed; sc["state"] = state; if (!reason.empty()) sc["reason"] = reason; sc["message"] = message; std::string text = std::string(ok ? "OK: " : "NOT OK: ") + action; if (!target.empty()) text += " " + target; if (!state.empty()) text += " (state=" + state + ")"; if (!reason.empty()) text += " reason=" + reason; return BuildStructuredResult(id, sc, text); } // ========== 分页与过滤 ========== struct Paging { int offset = 0; int limit = 100; }; bool ParsePaging(const Json::Value& id, const Json::Value& args, Paging& p, std::string& errJson) { if (!GetOptIntArg(id, args, "offset", 0, 100000000, p.offset, errJson)) return false; if (!GetOptIntArg(id, args, "limit", 1, 1000, p.limit, errJson)) return false; return true; } template Json::Value FilterAndPage(const Json::Value& all, Pred pred, const Paging& p, int& totalFiltered, bool& hasMore) { std::vector keep; keep.reserve(all.size()); for (Json::ArrayIndex i = 0; i < all.size(); ++i) { if (pred(all[i])) keep.push_back(all[i]); } totalFiltered = (int)keep.size(); int begin = p.offset < totalFiltered ? p.offset : totalFiltered; int end = begin + p.limit; if (end > totalFiltered) end = totalFiltered; Json::Value page(Json::arrayValue); for (int i = begin; i < end; ++i) page.append(keep[i]); hasMore = end < totalFiltered; return page; } // ========== 报文读取器(显式偏移 + 有界,禁止未对齐指针强转) ========== class Reader { public: explicit Reader(const std::vector& v) : m_p(v.data()), m_len(v.size()) {} size_t Offset() const { return m_off; } size_t Remaining() const { return m_off <= m_len ? m_len - m_off : 0; } bool Need(size_t n) const { return Remaining() >= n; } bool U8(BYTE& v) { if (!Need(1)) return false; v = m_p[m_off++]; return true; } bool U32(DWORD& v) { if (!Need(4)) return false; memcpy(&v, m_p + m_off, 4); m_off += 4; return true; } bool Dates(double& a, double& b) { if (!Need(16)) return false; memcpy(&a, m_p + m_off, 8); memcpy(&b, m_p + m_off + 8, 8); m_off += 16; return true; } bool Skip(size_t n) { if (!Need(n)) return false; m_off += n; return true; } // 剩余字节是否全为 0。客户端用 LocalSize() 决定发送长度,分配粒度可能带出少量 // 尾随 0 填充;记录解析循环据此提前结束,而不是把填充误判成截断。 bool AllZero() const { for (size_t i = m_off; i < m_len; ++i) { if (m_p[i] != 0) return false; } return true; } // 读取 NUL 结尾字符串(最多 maxBytes),推进 n + 1 + skipExtra 字节。 // 找不到 NUL 视为截断。 bool Str(UINT cp, size_t maxBytes, size_t skipExtra, std::string& out) { size_t avail = Remaining(); if (avail > maxBytes) avail = maxBytes; if (avail == 0) return false; size_t n = BoundedStrlen((const char*)m_p + m_off, avail); if (n == avail) return false; // 没有 NUL → 截断 if (!Need(n + 1 + skipExtra)) return false; // 尾随的 pad 字节缺失 out = ToUtf8((const char*)m_p + m_off, cp); m_off += n + 1 + skipExtra; return true; } // 定长字符串字段(如 char title[260]),字段内以 NUL 结尾或被 pad 填满。 bool FixedStr(UINT cp, size_t fixed, std::string& out) { if (!Need(fixed)) return false; char buf[1025]; size_t n = fixed < sizeof(buf) - 1 ? fixed : sizeof(buf) - 1; memcpy(buf, m_p + m_off, n); buf[n] = 0; out = ToUtf8(buf, cp); m_off += fixed; return true; } private: const BYTE* m_p; size_t m_len; size_t m_off = 0; }; // ========== 解析:主机管理各 tab ========== bool ParseProcesses(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; // 只有 token = 空列表 Reader r(data); if (!r.Skip(1)) { err = "empty process packet"; return false; } while (r.Remaining() > 0) { if (r.AllZero()) break; // 末尾 0 填充 size_t start = r.Offset(); DWORD pid = 0; BYTE is64 = 0; std::string title, exe, path, user; DWORD pri = 0, threads = 0, workingSetKb = 0, fileSizeKb = 0; if (!r.U32(pid) || !r.U8(is64) || !r.FixedStr(cp, MAX_PATH, title) || !r.Str(cp, r.Remaining(), 1, exe) || !r.Str(cp, r.Remaining(), 1, path) || !r.U32(pri) || !r.U32(threads) || !r.Str(cp, r.Remaining(), 1, user) || !r.U32(workingSetKb) || !r.U32(fileSizeKb)) { err = "truncated process record at offset " + std::to_string(start); return false; } Json::Value item(Json::objectValue); item["pid"] = (Json::UInt)pid; item["arch"] = is64 ? "x64" : "x86"; item["title"] = title; item["name"] = exe; item["path"] = path; item["priorityClass"] = (Json::UInt)pri; item["priority"] = PriorityName(pri); item["threads"] = (Json::UInt)threads; item["user"] = user; item["workingSetKb"] = (Json::UInt)workingSetKb; item["fileSizeKb"] = (Json::UInt)fileSizeKb; out.append(item); } return true; } bool ParseWindows(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; size_t payload = data.size() - 1; // 记录是定长的;若末尾有不足一条记录的多余字节(分配粒度带来的 0 填充),忽略即可。 size_t count = payload / sizeof(WINDOWSINFO); for (size_t i = 0; i < count; ++i) { WINDOWSINFO info; memcpy(&info, data.data() + 1 + i * sizeof(WINDOWSINFO), sizeof(info)); char title[sizeof(info.strTitle) + 1] = { 0 }; memcpy(title, info.strTitle, sizeof(info.strTitle)); Json::Value item(Json::objectValue); item["hwnd"] = (Json::UInt)info.m_hwnd; item["title"] = ToUtf8(title, cp); item["pid"] = (Json::UInt)info.m_poceessid; item["width"] = info.w; item["height"] = info.h; item["canSee"] = info.canlook ? true : false; out.append(item); } return true; } bool ParseNetState(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; Reader r(data); if (!r.Skip(1)) { err = "empty netstate packet"; return false; } while (r.Remaining() > 0) { if (r.AllZero()) break; // 末尾 0 填充 size_t start = r.Offset(); std::string proc, proto, local, remote, state; DWORD pid = 0; if (!r.Str(cp, r.Remaining(), 1, proc) || !r.U32(pid) || !r.Skip(2) || !r.Str(cp, r.Remaining(), 1, proto) || !r.Str(cp, r.Remaining(), 1, local) || !r.Str(cp, r.Remaining(), 1, remote) || !r.Str(cp, r.Remaining(), 1, state)) { err = "truncated netstate record at offset " + std::to_string(start); return false; } std::string protocol = (proto == "[UDP]") ? "udp" : "tcp"; Json::Value item(Json::objectValue); item["process"] = proc; item["pid"] = (Json::UInt)pid; item["protocol"] = protocol; item["localEndpoint"] = local; item["localAddress"] = EndpointAddr(local); item["localPort"] = EndpointPort(local); item["remoteEndpoint"] = remote; item["remoteAddress"] = EndpointAddr(remote); item["remotePort"] = EndpointPort(remote); item["state"] = Trim(state); out.append(item); } return true; } bool ParseSoftware(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; Reader r(data); if (!r.Skip(1)) { err = "empty software packet"; return false; } while (r.Remaining() > 0) { if (r.AllZero()) break; // 末尾 0 填充 size_t start = r.Offset(); std::string name, publisher, version, installDate, uninstall; if (!r.Str(cp, r.Remaining(), 1, name) || !r.Str(cp, r.Remaining(), 1, publisher) || !r.Str(cp, r.Remaining(), 1, version) || !r.Str(cp, r.Remaining(), 1, installDate) || !r.Str(cp, r.Remaining(), 1, uninstall)) { err = "truncated software record at offset " + std::to_string(start); return false; } Json::Value item(Json::objectValue); item["name"] = name; item["publisher"] = publisher; item["version"] = version; item["installDate"] = installDate; item["uninstallString"] = uninstall; out.append(item); } return true; } bool ParseBrowserHistory(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; size_t payload = data.size() - 1; const size_t kRec = 100 + 1024 + 1024; // Browsinghistory size_t count = payload / kRec; // 末尾不足一条记录的字节忽略 for (size_t i = 0; i < count; ++i) { const BYTE* p = data.data() + 1 + i * kRec; char t[101] = { 0 }, title[1025] = { 0 }, url[1025] = { 0 }; memcpy(t, p, 100); memcpy(title, p + 100, 1024); memcpy(url, p + 100 + 1024, 1024); Json::Value item(Json::objectValue); item["time"] = t; // 客户端已格式化为 ASCII 时间 item["title"] = ToUtf8(title, cp); item["url"] = ToUtf8(url, cp); out.append(item); } return true; } bool ParseFavorites(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; Reader r(data); if (!r.Skip(1)) { err = "empty favorites packet"; return false; } while (r.Remaining() > 0) { if (r.AllZero()) break; // 末尾 0 填充 size_t start = r.Offset(); std::string name, url; if (!r.Str(cp, r.Remaining(), 1, name) || !r.Str(cp, r.Remaining(), 1, url)) { err = "truncated favorites record at offset " + std::to_string(start); return false; } Json::Value item(Json::objectValue); item["name"] = name; item["url"] = url; out.append(item); } return true; } bool ParseServices(const std::vector& data, UINT cp, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 1) return true; Reader r(data); if (!r.Skip(1)) { err = "empty service packet"; return false; } while (r.Remaining() > 0) { if (r.AllZero()) break; // 末尾 0 填充 size_t start = r.Offset(); std::string displayName, description, state, startType, login, interactive, name, binaryPath; if (!r.Str(cp, r.Remaining(), 0, displayName) || !r.Str(cp, r.Remaining(), 0, description) || !r.Str(cp, r.Remaining(), 0, state) || !r.Str(cp, r.Remaining(), 0, startType) || !r.Str(cp, r.Remaining(), 0, login) || !r.Str(cp, r.Remaining(), 0, interactive) || !r.Str(cp, r.Remaining(), 0, name) || !r.Str(cp, r.Remaining(), 0, binaryPath)) { err = "truncated service record at offset " + std::to_string(start); return false; } Json::Value item(Json::objectValue); item["displayName"] = displayName; item["description"] = description; item["state"] = state; item["startType"] = startType; item["login"] = login; item["interactive"] = (interactive == u8"是"); item["name"] = name; item["binaryPath"] = binaryPath; out.append(item); } return true; } // 任务记录:name/path/exe 为 UTF-8,status 为 ANSI(GBK),各以 NUL 结尾; // 之后两个 DATE(double)。列表以两个 0 字节结尾。 bool ParseTasks(const std::vector& data, Json::Value& out, std::string& err) { out = Json::Value(Json::arrayValue); if (data.size() <= 2) return true; // 只有 token(可能带 1 个结束 0) Reader r(data); if (!r.Skip(1)) { err = "empty task packet"; return false; } while (r.Remaining() > 2) { if (data[r.Offset()] == 0) break; // 结束标记(两个 0 字节) size_t start = r.Offset(); std::string name, path, exe, status; double last = 0, next = 0; if (!r.Str(CP_UTF8, r.Remaining(), 0, name) || !r.Str(CP_UTF8, r.Remaining(), 0, path) || !r.Str(CP_UTF8, r.Remaining(), 0, exe) || !r.Str(936, r.Remaining(), 0, status) || !r.Dates(last, next)) { err = "truncated task record at offset " + std::to_string(start); return false; } if (name.empty()) break; // 防御:空名视为结束 // folder = path 去掉末尾 "\";不匹配时退回最后一个反斜杠之前。 std::string folder; std::string suffix = "\\" + name; if (path.size() >= suffix.size() && _stricmp(path.c_str() + path.size() - suffix.size(), suffix.c_str()) == 0) { folder = path.substr(0, path.size() - suffix.size()); } else { size_t pos = path.rfind('\\'); folder = (pos == std::string::npos) ? "\\" : path.substr(0, pos); } if (folder.empty()) folder = "\\"; Json::Value item(Json::objectValue); item["name"] = name; item["path"] = path; item["folder"] = folder; item["exe"] = exe; item["status"] = status; item["lastRun"] = OleDateToStr(last); item["nextRun"] = OleDateToStr(next); out.append(item); } return true; } // ========== 解析:连接管理 ========== std::string FormatAddr(uint8_t af, const uint8_t* addr) { char buf[INET6_ADDRSTRLEN] = { 0 }; if (af == AF_INET) { inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN); } else if (af == AF_INET6) { inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN); } return buf; } struct ConnRecord { const BYTE* raw = nullptr; // 指向 data 中的原始 110 字节(强制断开原样回传) int proto = 0; int af = 0; uint32_t pid = 0; uint8_t localAddr[16] = { 0 }; uint16_t localPort = 0; uint8_t remoteAddr[16] = { 0 }; uint16_t remotePort = 0; uint32_t state = 0; std::string procName; }; // 解析连接列表。data 首字节必须是 TOKEN_CONNECTION_LIST。 bool ParseConnections(const std::vector& data, std::vector& records, std::string& err) { records.clear(); if (data.empty()) { err = "empty connection packet"; return false; } if (data[0] != TOKEN_CONNECTION_LIST) { err = "unexpected token " + std::to_string((int)data[0]) + " for connection list"; return false; } if (data.size() < 3) { err = "connection packet too short"; return false; } uint16_t count = 0; memcpy(&count, data.data() + 1, 2); size_t need = 3 + (size_t)count * kConnRecordSize; if (data.size() < need) { err = "connection packet truncated: declared " + std::to_string(count) + " records but only " + std::to_string(data.size() - 3) + " bytes"; return false; } for (uint16_t i = 0; i < count; ++i) { const BYTE* p = data.data() + 3 + (size_t)i * kConnRecordSize; ConnRecord rec; rec.raw = p; rec.proto = p[0]; rec.af = p[1]; memcpy(&rec.pid, p + 2, 4); memcpy(rec.localAddr, p + 6, 16); memcpy(&rec.localPort, p + 22, 2); memcpy(rec.remoteAddr, p + 24, 16); memcpy(&rec.remotePort, p + 40, 2); memcpy(&rec.state, p + 42, 4); char name[65] = { 0 }; memcpy(name, p + 46, 64); rec.procName = name; // 客户端写的是 UTF-8 records.push_back(rec); } return true; } Json::Value ConnRecordToJson(const ConnRecord& rec) { Json::Value item(Json::objectValue); item["protocol"] = (rec.proto == 1) ? "tcp" : (rec.proto == 2 ? "udp" : "unknown"); item["family"] = (rec.af == AF_INET) ? "ipv4" : (rec.af == AF_INET6 ? "ipv6" : "unknown"); item["pid"] = (Json::UInt)rec.pid; item["process"] = rec.procName; item["localAddress"] = FormatAddr((uint8_t)rec.af, rec.localAddr); item["localPort"] = (Json::UInt)rec.localPort; item["remoteAddress"] = FormatAddr((uint8_t)rec.af, rec.remoteAddr); item["remotePort"] = (Json::UInt)rec.remotePort; item["state"] = (Json::UInt)rec.state; item["stateName"] = (rec.proto == 1) ? TcpStateName(rec.state) : ""; item["killable"] = (rec.proto == 1 && rec.af == AF_INET); return item; } // ========== 主机解析 / 会话封装 ========== struct HostTool { uint64_t dev = 0; context* mainCtx = nullptr; }; // requireWindows_ = true 时对 LNX/MAC/APK 直接返回 -32005,避免白等超时。 bool ResolveHost(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent, HostTool& ht, bool requireWindows_, std::string& errJson) { uint64_t dev = 0; std::string err; if (!ParseHostIdArg(args, dev, err)) { errJson = BuildError(id, -32602, err); return false; } context* ctx = FindMainContext(parent, dev); if (!ctx) { errJson = BuildError(id, kErrOffline, "Host not found or offline: " + std::to_string(dev)); return false; } if (requireWindows_ && !IsWindowsClient(ctx)) { errJson = BuildError(id, kErrUnsupported, "This tool is only supported on Windows hosts"); return false; } ht.dev = dev; ht.mainCtx = ctx; return true; } std::string WaitFailJson(const Json::Value& id, CMcpServer::ManagedWaitResult r, const std::string& clientErr, const char* what, bool readyPhase) { switch (r) { case CMcpServer::ManagedWaitResult::ClientError: return BuildError(id, kErrGeneric, std::string("Client reported an error: ") + (clientErr.empty() ? std::string("(no detail)") : clientErr)); case CMcpServer::ManagedWaitResult::Closed: return BuildError(id, kErrTimeout, std::string("Sub-connection closed while waiting for ") + what); case CMcpServer::ManagedWaitResult::Timeout: default: { std::string msg = std::string("Timeout waiting for ") + what; if (readyPhase) { msg += " (host may be offline, the feature may be disabled on the client, " "or the client is too old to support it)"; } return BuildError(id, kErrTimeout, msg); } } } // 一次主机管理 / 连接管理会话。析构时若未正常收尾,按失败路径打墓碑并关子连接。 class ManagedSession { public: ManagedSession() {} ~ManagedSession() { Abort(); } ManagedSession(const ManagedSession&) = delete; ManagedSession& operator=(const ManagedSession&) = delete; bool Open(const Json::Value& id, const std::string& tool, const HostTool& ht, bool connMgr, std::vector* firstPacket, std::string& errJson) { m_dev = ht.dev; CMcpServer& mcp = McpServer(); bool began = connMgr ? mcp.BeginConnMgrSession(m_dev, tool, ht.mainCtx) : mcp.BeginMachineSession(m_dev, tool, ht.mainCtx); if (!began) { errJson = BuildError(id, kErrBusy, "Device busy: another MCP request is pending for this host"); return false; } m_open = true; BYTE cmd = connMgr ? (BYTE)COMMAND_CONNECTION : (BYTE)COMMAND_MACHINE_MANAGE; if (!ht.mainCtx->Send2Client(&cmd, 1)) { errJson = BuildError(id, kErrSend, "Failed to send command to host"); return false; // 析构 → Abort(打墓碑) } BYTE readyToken = connMgr ? (BYTE)TOKEN_CONNECTION_LIST : (BYTE)TOKEN_SYSINFOLIST; std::vector ready; std::string clientErr; CMcpServer::ManagedWaitResult r = mcp.WaitManagedPacket(m_dev, readyToken, ready, m_sub, kReadyTimeoutMs, clientErr); if (r != CMcpServer::ManagedWaitResult::Ok) { errJson = WaitFailJson(id, r, clientErr, connMgr ? "connection list" : "machine manager readiness", true); return false; } if (firstPacket) *firstPacket = ready; return true; } // 发 1 字节 tab 请求并等待指定响应 token。 bool Request(const Json::Value& id, BYTE req, BYTE respToken, std::vector& out, std::string& errJson) { CMcpServer& mcp = McpServer(); mcp.ArmManagedResponse(m_dev, respToken); if (!m_sub || !m_sub->Send2Client(&req, 1)) { errJson = BuildError(id, kErrSend, "Failed to send request to host"); return false; } std::string clientErr; CMcpServer::ManagedWaitResult r = mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kListTimeoutMs, clientErr); if (r != CMcpServer::ManagedWaitResult::Ok) { errJson = WaitFailJson(id, r, clientErr, "host management response", false); return false; } return true; } // 发一个动作包并等待指定的刷新响应(客户端自动回)。 bool Action(const Json::Value& id, std::vector& pkt, BYTE respToken, std::vector& out, std::string& errJson) { CMcpServer& mcp = McpServer(); mcp.ArmManagedResponse(m_dev, respToken); if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) { errJson = BuildError(id, kErrSend, "Failed to send action to host"); return false; } m_sub->WaitSendIdle(2000); std::string clientErr; CMcpServer::ManagedWaitResult r = mcp.WaitManagedPacket(m_dev, respToken, out, m_sub, kActionTimeoutMs, clientErr); if (r != CMcpServer::ManagedWaitResult::Ok) { errJson = WaitFailJson(id, r, clientErr, "action confirmation", false); return false; } return true; } // 发一个无响应的动作包(进程冻结/解冻、窗口操作)。 bool SendNoReply(const Json::Value& id, std::vector& pkt, std::string& errJson) { if (!m_sub || !m_sub->Send2Client(pkt.data(), (ULONG)pkt.size())) { errJson = BuildError(id, kErrSend, "Failed to send action to host"); return false; } m_sub->WaitSendIdle(2000); return true; } void Finish() { if (!m_open) return; McpServer().EndManagedSession(m_dev, true, false); m_open = false; } void Abort() { if (!m_open) return; McpServer().EndManagedSession(m_dev, true, true); m_open = false; } uint64_t Dev() const { return m_dev; } context* Sub() const { return m_sub; } private: bool m_open = false; uint64_t m_dev = 0; context* m_sub = nullptr; }; // 目标过期:-32009,提示调用方刷新快照后重试。 std::string NotFoundJson(const Json::Value& id, const std::string& what) { return BuildError(id, kErrNotFound, what + " does not exist in the latest snapshot; refresh the list and retry"); } } // namespace // ============================================================================ // CMcpServer:P8 会话状态机(在 UI 线程 / 工具线程之间共享) // ============================================================================ bool CMcpServer::KindAcceptsToken(PendingKind kind, BYTE token) { if (kind == PendingKind::Machine) return token == (BYTE)TOKEN_SYSINFOLIST; if (kind == PendingKind::ConnMgr) return token == (BYTE)TOKEN_CONNECTION_LIST; return false; } void CMcpServer::RefreshManagedRoutesLocked() { m_ManagedRoutes.store((int)(m_SubCtxToDevice.size() + m_AbsorbedSubCtx.size()), std::memory_order_relaxed); } void CMcpServer::PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now) { for (auto it = m_OrphanByDevice.begin(); it != m_OrphanByDevice.end();) { if (it->second.expiry <= now) it = m_OrphanByDevice.erase(it); else ++it; } for (auto it = m_OrphanByPeer.begin(); it != m_OrphanByPeer.end();) { if (it->second.expiry <= now) it = m_OrphanByPeer.erase(it); else ++it; } } bool CMcpServer::BeginManagedSession(uint64_t device_id, const std::string& tool, PendingKind kind, context* mainCtx) { if (IsFileTransferPending(device_id)) return false; // 文件传输在飞:单设备单飞行 std::lock_guard lk(m_PendingMutex); if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙 PendingRequest r; r.tool = tool; r.kind = kind; r.mainCtx = mainCtx; r.peer = mainCtx ? mainCtx->GetPeerName() : ""; r.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kSessionDeadlineMs); m_Pending[device_id] = std::move(r); return true; } bool CMcpServer::BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx) { return BeginManagedSession(device_id, tool, PendingKind::Machine, mainCtx); } bool CMcpServer::BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx) { return BeginManagedSession(device_id, tool, PendingKind::ConnMgr, mainCtx); } void CMcpServer::ArmManagedResponse(uint64_t device_id, BYTE expectedToken) { std::lock_guard lk(m_PendingMutex); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return; if (it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr) return; it->second.expectedToken = expectedToken; it->second.hasResponse = false; it->second.response.clear(); it->second.clientError.clear(); } CMcpServer::ManagedWaitResult CMcpServer::WaitManagedPacket(uint64_t device_id, BYTE token, std::vector& out, context*& subCtxOut, int timeoutMs, std::string& clientError) { std::unique_lock lk(m_PendingMutex); auto waitUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); // 循环等待:迟到的/无关的包被丢弃后继续等,直到本次步骤超时或会话截止时间。 for (;;) { auto it = m_Pending.find(device_id); if (it == m_Pending.end()) return ManagedWaitResult::Closed; PendingRequest& r = it->second; if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) return ManagedWaitResult::Closed; subCtxOut = r.subCtx; // 绑定可能发生在等待期间 if (!r.clientError.empty()) { clientError = r.clientError; r.clientError.clear(); return ManagedWaitResult::ClientError; } if (r.hasResponse) { bool match = !r.response.empty() && r.response[0] == token; if (match) { out = std::move(r.response); r.hasResponse = false; return ManagedWaitResult::Ok; } // 期望 token 之外的响应(例如客户端主动发来的提示):丢弃后继续等。 r.hasResponse = false; r.response.clear(); } if (r.closed) return ManagedWaitResult::Closed; if (waitUntil > r.deadline) waitUntil = r.deadline; auto now = std::chrono::steady_clock::now(); if (now >= waitUntil) return ManagedWaitResult::Timeout; m_PendingCv.wait_until(lk, waitUntil); } } void CMcpServer::EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan) { context* sub = nullptr; PendingKind kind = PendingKind::Generic; std::string peer; { std::lock_guard lk(m_PendingMutex); auto now = std::chrono::steady_clock::now(); auto it = m_Pending.find(device_id); if (it == m_Pending.end()) { PruneOrphanMarksLocked(now); return; } PendingRequest& r = it->second; if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) return; sub = r.subCtx; kind = r.kind; peer = r.peer; if (sub) { m_SubCtxToDevice.erase(sub); // MCP 关掉的子连接上仍可能有在途包:登记为吸收,直到 OfflineProc 回收。 if (cancelIo) m_AbsorbedSubCtx.insert(sub); } m_Pending.erase(it); if (markOrphan) { OrphanMark mk; mk.expiry = now + std::chrono::seconds(5); mk.kind = kind; m_OrphanByDevice[device_id] = mk; if (!peer.empty()) m_OrphanByPeer[peer] = mk; } PruneOrphanMarksLocked(now); RefreshManagedRoutesLocked(); } Mprintf("[MCP-P8] EndManagedSession dev=%llu kind=%d sub=%p cancelIo=%d orphan=%d\n", device_id, (int)kind, (void*)sub, cancelIo ? 1 : 0, markOrphan ? 1 : 0); if (sub && cancelIo) sub->CancelIO(); // 锁外关子连接 } bool CMcpServer::OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len) { if (!subCtx || !data || len == 0) return false; BYTE token = data[0]; bool readyToken = (token == (BYTE)TOKEN_SYSINFOLIST) || (token == (BYTE)TOKEN_CONNECTION_LIST); // 快速路径:没有已绑定/已吸收的子连接,且不是就绪包 → 与 P8 无关。 // MessageHandle 每个包都会调用本函数(含音频/屏幕等高频子连接),不能逐包抢锁。 if (!readyToken && m_ManagedRoutes.load(std::memory_order_relaxed) == 0) return false; // 主连接自身不会发就绪包;若 clientID 指向的“主连接”就是本 ctx,回落 MFC。 // FindMainContext 要取 parent->m_cs,必须在持 m_PendingMutex 之前完成, // 维持锁序 m_cs → m_PendingMutex(与 P2/P7 的调用顺序一致),避免反向等待。 uint64_t cid = subCtx->GetClientID(); bool mainIsSelf = readyToken && cid != 0 && FindMainContext(m_parent, cid) == subCtx; std::unique_lock lk(m_PendingMutex); // ---- 情形 1:已绑定的子连接 → 该连接上的包全部由 MCP 消费 ---- auto bindIt = m_SubCtxToDevice.find(subCtx); if (bindIt != m_SubCtxToDevice.end()) { uint64_t dev = bindIt->second; auto it = m_Pending.find(dev); if (it == m_Pending.end() || (it->second.kind != PendingKind::Machine && it->second.kind != PendingKind::ConnMgr)) { // 会话已结束但包还在路上:吸收并断开 m_SubCtxToDevice.erase(bindIt); m_AbsorbedSubCtx.insert(subCtx); RefreshManagedRoutesLocked(); lk.unlock(); Mprintf("[MCP-P8] bound sub=%p without session: absorb+close\n", (void*)subCtx); subCtx->CancelIO(); return true; } PendingRequest& r = it->second; if (len > kMaxPacketBytes) { r.clientError = "response packet too large (" + std::to_string(len) + " bytes)"; m_PendingCv.notify_all(); lk.unlock(); subCtx->CancelIO(); return true; } if (token == (BYTE)TOKEN_MACHINE_MSG) { // 客户端主动报错(CP936 文本) r.clientError = ToUtf8((const char*)data + 1, 936); m_PendingCv.notify_all(); return true; } if (r.expectedToken != 0 && token == r.expectedToken) { r.response.assign(data, data + len); r.hasResponse = true; m_PendingCv.notify_all(); } // 其它包(迟到/无关)丢弃 return true; } // ---- 情形 2:已吸收(CancelIO 后仍到达的尾包)→ 丢弃 ---- if (m_AbsorbedSubCtx.find(subCtx) != m_AbsorbedSubCtx.end()) { lk.unlock(); subCtx->CancelIO(); return true; } // ---- 情形 3:未绑定且不是就绪包 → 交回 MFC ---- if (!readyToken) return false; if (mainIsSelf) return false; PendingKind wantKind = (token == (BYTE)TOKEN_SYSINFOLIST) ? PendingKind::Machine : PendingKind::ConnMgr; // 3a. 按 clientID 精确匹配(新客户端经 TOKEN_CONN_AUTH 已钉 ID) uint64_t boundDev = 0; if (cid != 0) { auto it = m_Pending.find(cid); if (it != m_Pending.end() && it->second.kind == wantKind && it->second.subCtx == nullptr) boundDev = cid; } std::string peer = subCtx->GetPeerName(); // 3b. 老客户端子连接 clientID 可能为 0:按 peer 兜底,仅在同 peer 唯一候选时绑定 if (boundDev == 0 && cid == 0 && !peer.empty()) { uint64_t candidate = 0; int matches = 0; for (auto& kv : m_Pending) { if (kv.second.kind != wantKind) continue; if (kv.second.subCtx != nullptr) continue; if (kv.second.peer == peer) { candidate = kv.first; ++matches; } } if (matches == 1) boundDev = candidate; } if (boundDev == 0) { // 3c. 没有等待中的会话:若刚有会话异常结束(5s 墓碑),吸收这个迟到的就绪包, // 避免弹出 MFC 对话框。 auto now = std::chrono::steady_clock::now(); PruneOrphanMarksLocked(now); bool absorb = false; if (cid != 0) { auto o = m_OrphanByDevice.find(cid); if (o != m_OrphanByDevice.end() && o->second.expiry > now && KindAcceptsToken(o->second.kind, token)) absorb = true; } else if (!peer.empty()) { auto o = m_OrphanByPeer.find(peer); if (o != m_OrphanByPeer.end() && o->second.expiry > now && KindAcceptsToken(o->second.kind, token)) absorb = true; } if (absorb) { m_AbsorbedSubCtx.insert(subCtx); RefreshManagedRoutesLocked(); lk.unlock(); Mprintf("[MCP-P8] late ready token=%d peer=%s cid=%llu: absorb+close\n", (int)token, peer.c_str(), (unsigned long long)cid); subCtx->CancelIO(); return true; } return false; // 用户手动打开对话框 } // ---- 绑定 ---- PendingRequest& r = m_Pending[boundDev]; r.subCtx = subCtx; m_SubCtxToDevice[subCtx] = boundDev; RefreshManagedRoutesLocked(); if (len > kMaxPacketBytes) { r.clientError = "readiness packet too large (" + std::to_string(len) + " bytes)"; m_PendingCv.notify_all(); lk.unlock(); subCtx->CancelIO(); return true; } r.response.assign(data, data + len); r.hasResponse = true; m_PendingCv.notify_all(); Mprintf("[MCP-P8] bind dev=%llu tool=%s sub=%p token=%d len=%d\n", boundDev, r.tool.c_str(), (void*)subCtx, (int)token, (int)len); return true; } void CMcpServer::OnManagedContextClosed(context* subCtx) { if (!subCtx) return; std::lock_guard lk(m_PendingMutex); m_AbsorbedSubCtx.erase(subCtx); m_SubCtxToDevice.erase(subCtx); for (auto& kv : m_Pending) { PendingRequest& r = kv.second; if (r.kind != PendingKind::Machine && r.kind != PendingKind::ConnMgr) continue; if (r.subCtx == subCtx || r.mainCtx == subCtx) { Mprintf("[MCP-P8] context closed dev=%llu tool=%s: wake waiter\n", kv.first, r.tool.c_str()); if (r.subCtx == subCtx) { // 子连接已死:不要再让超时路径对它 CancelIO r.subCtx = nullptr; } r.closed = true; m_PendingCv.notify_all(); } } RefreshManagedRoutesLocked(); } // ============================================================================ // 连接管理 // ============================================================================ namespace { bool ListConnections(const Json::Value& id, const HostTool& ht, const std::string& toolName, std::vector& records, std::string& errJson) { ManagedSession session; std::vector first; if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) return false; if (!ParseConnections(first, records, errJson)) { errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson); return false; } session.Finish(); return true; } std::string BuildListNetworkConnections(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string protocol = ToLowerAscii(OptStr(args, "protocol")); if (protocol.empty() || protocol == "all") protocol = "all"; else if (protocol != "tcp" && protocol != "udp") { return BuildError(id, -32602, "Invalid protocol: expected tcp, udp or all"); } std::string family = ToLowerAscii(OptStr(args, "family")); if (family.empty() || family == "all") family = "all"; else if (family != "ipv4" && family != "ipv6") { return BuildError(id, -32602, "Invalid family: expected ipv4, ipv6 or all"); } int pid = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pid, errJson)) return errJson; int localPort = -1, remotePort = -1; if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; std::string process = OptStr(args, "process"); std::string remoteAddr = OptStr(args, "remote_address"); std::string state = OptStr(args, "state"); std::vector records; if (!ListConnections(id, ht, "list_network_connections", records, errJson)) return errJson; Json::Value all(Json::arrayValue); for (const ConnRecord& rec : records) all.append(ConnRecordToJson(rec)); auto pred = [&](const Json::Value& item) { if (protocol != "all" && item["protocol"].asString() != protocol) return false; if (family != "all" && item["family"].asString() != family) return false; if (pid >= 0 && item["pid"].asUInt() != (Json::UInt)pid) return false; if (localPort >= 0 && item["localPort"].asUInt() != (Json::UInt)localPort) return false; if (remotePort >= 0 && item["remotePort"].asUInt() != (Json::UInt)remotePort) return false; if (!MatchCI(item["process"].asString(), process)) return false; if (!remoteAddr.empty() && !ContainsCI(item["remoteAddress"].asString(), remoteAddr)) return false; if (!state.empty()) { if (IsDigits(state)) { if (item["state"].asUInt() != (Json::UInt)atoi(state.c_str())) return false; } else if (!ContainsCI(item["stateName"].asString(), state)) { return false; } } return true; }; int totalAll = (int)all.size(); int totalFiltered = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, totalFiltered, hasMore); std::string text = std::string(u8"共 ") + std::to_string(totalFiltered) + u8" 条连接(库中 " + std::to_string(totalAll) + u8" 条,本次返回 " + std::to_string(page.size()) + u8" 条)"; return BuildListResult(id, "connections", page, totalFiltered, totalAll, paging.offset, paging.limit, hasMore, text); } // 解析 IPv4 点分十进制 → 4 字节;仅接受严格 IPv4。 bool ParseIPv4(const std::string& s, uint8_t out[4]) { return inet_pton(AF_INET, s.c_str(), out) == 1; } std::string BuildKillNetworkConnection(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { CMcpServer& mcp = McpServer(); if (!mcp.IsManageEnabled() || mcp.IsReadonly()) { return BuildError(id, kErrDisabled, "kill_network_connection is disabled (requires McpManage=1 and McpReadonly=0)"); } HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, /*requireWindows=*/true, errJson)) return errJson; std::string protocol = ToLowerAscii(OptStr(args, "protocol")); if (protocol.empty()) protocol = "tcp"; if (protocol != "tcp" && protocol != "udp") return BuildError(id, -32602, "Invalid protocol: expected tcp or udp"); std::string localAddrStr = OptStr(args, "local_address"); std::string remoteAddrStr = OptStr(args, "remote_address"); if (localAddrStr.empty() || remoteAddrStr.empty()) return BuildError(id, -32602, "local_address and remote_address are required"); int localPort = 0, remotePort = 0; if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; if (!args.isMember("local_port") || !args.isMember("remote_port")) return BuildError(id, -32602, "local_port and remote_port are required"); int pidFilter = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; bool dryRun = GetBoolArg(args, "dry_run", false); uint8_t localBytes[4] = { 0 }, remoteBytes[4] = { 0 }; if (!ParseIPv4(localAddrStr, localBytes) || !ParseIPv4(remoteAddrStr, remoteBytes)) return BuildError(id, kErrUnsupported, "Only IPv4 addresses are supported for forced disconnect " "(the client can only drop IPv4 TCP connections)"); std::string toolName = "kill_network_connection"; std::vector records; if (!ListConnections(id, ht, toolName, records, errJson)) return errJson; // 精确匹配:协议 + 地址字节 + 端口(+ 可选 pid)。用客户端上报的原始记录,不重拼地址。 std::string target = protocol + " " + localAddrStr + ":" + std::to_string(localPort) + " -> " + remoteAddrStr + ":" + std::to_string(remotePort); const ConnRecord* found = nullptr; int matches = 0; for (const ConnRecord& rec : records) { int wantProto = (protocol == "tcp") ? 1 : 2; if (rec.proto != wantProto) continue; if (rec.af != AF_INET) continue; if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; if (rec.localPort != (uint16_t)localPort) continue; if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; if (rec.remotePort != (uint16_t)remotePort) continue; if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; found = &rec; ++matches; } if (matches == 0) { AuditManageAction(parent, ht.dev, toolName, target + " result=not_found"); return NotFoundJson(id, "connection " + target); } if (matches > 1) { return BuildError(id, -32602, "Ambiguous target: " + std::to_string(matches) + " connections match; specify pid to disambiguate"); } Json::Value targetJson = ConnRecordToJson(*found); if (found->proto != 1 || found->af != AF_INET) { return BuildError(id, kErrUnsupported, "Only IPv4 TCP connections can be forcibly disconnected"); } if (pidFilter < 0) pidFilter = (int)found->pid; if (dryRun) { Json::Value sc(Json::objectValue); sc["ok"] = true; sc["dryRun"] = true; sc["target"] = target; sc["connection"] = targetJson; AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) + " result=dry_run"); return BuildStructuredResult(id, sc, "Dry run: target found, nothing was sent"); } // 会话保持:取列表用的是第一条 255,断开后客户端会自动再回一条 255。 ManagedSession session; std::vector first; if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) return errJson; std::vector pre; if (!ParseConnections(first, pre, errJson)) { errJson = BuildError(id, kErrGeneric, "Malformed connection list: " + errJson); return errJson; } // 重新在最新列表里定位(模板绑定期间列表可能变化;原始记录也必须来自最新包) const ConnRecord* latest = nullptr; int latestMatches = 0; for (const ConnRecord& rec : pre) { if (rec.proto != 1 || rec.af != AF_INET) continue; if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; if (rec.localPort != (uint16_t)localPort) continue; if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; if (rec.remotePort != (uint16_t)remotePort) continue; if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; latest = &rec; ++latestMatches; } if (latestMatches != 1) { AuditManageAction(parent, ht.dev, toolName, target + " result=not_found(stale)"); return latestMatches == 0 ? NotFoundJson(id, "connection " + target) : BuildError(id, -32602, "Ambiguous target in the latest snapshot; specify pid"); } std::vector pkt; pkt.reserve(1 + kConnRecordSize); pkt.push_back((BYTE)COMMAND_KILL_CONNECTION); pkt.insert(pkt.end(), latest->raw, latest->raw + kConnRecordSize); std::vector after; if (!session.Action(id, pkt, (BYTE)TOKEN_CONNECTION_LIST, after, errJson)) { AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) + " result=send_or_timeout"); return errJson; } std::vector post; std::string perr; if (!ParseConnections(after, post, perr)) { AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) + " result=bad_refresh"); return BuildError(id, kErrGeneric, "The action was sent but the refreshed list could not be parsed: " + perr); } bool stillPresent = false; for (const ConnRecord& rec : post) { if (rec.proto != 1 || rec.af != AF_INET) continue; if (memcmp(rec.localAddr, localBytes, 4) != 0) continue; if (rec.localPort != (uint16_t)localPort) continue; if (memcmp(rec.remoteAddr, remoteBytes, 4) != 0) continue; if (rec.remotePort != (uint16_t)remotePort) continue; if (pidFilter >= 0 && rec.pid != (uint32_t)pidFilter) continue; stillPresent = true; break; } session.Finish(); AuditManageAction(parent, ht.dev, toolName, target + " pid=" + std::to_string(pidFilter) + (stillPresent ? " result=still_present" : " result=disconnected")); if (stillPresent) { return BuildActionResult(id, false, true, false, "kill", target, "present", "still_present", u8"连接仍在:客户端可能没有管理员权限(SetTcpEntry 失败)," u8"或该连接已被系统重建。"); } return BuildActionResult(id, true, true, true, "kill", target, "removed", "", u8"连接已断开(刷新后的连接列表中四元组已消失)。"); } } // namespace // ============================================================================ // 主机管理:读工具 // ============================================================================ namespace { // 打开主机管理会话 → 发 1 字节 tab → 取响应 → 结束会话。 bool FetchMachineTab(const Json::Value& id, const HostTool& ht, const std::string& toolName, BYTE tab, BYTE respToken, std::vector& out, std::string& errJson) { ManagedSession session; if (!session.Open(id, toolName, ht, /*connMgr=*/false, nullptr, errJson)) return false; if (!session.Request(id, tab, respToken, out, errJson)) return false; session.Finish(); return true; } std::string ParseErrorToJson(const Json::Value& id, const char* what, const std::string& detail) { return BuildError(id, kErrGeneric, std::string("Malformed ") + what + " response: " + detail); } } // namespace // ---- host_list_processes ---- static std::string BuildHostListProcesses(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; int pidFilter = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; std::string name = OptStr(args, "name"); std::string arch = ToLowerAscii(OptStr(args, "arch")); if (!arch.empty() && arch != "x64" && arch != "x86") return BuildError(id, -32602, "Invalid arch: expected x64 or x86"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_processes", (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseProcesses(data, 936, all, perr)) return ParseErrorToJson(id, "process list", perr); auto pred = [&](const Json::Value& item) { if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; if (!MatchCI(item["name"].asString(), name)) return false; if (!arch.empty() && item["arch"].asString() != arch) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个进程(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "processes", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_windows ---- static std::string BuildHostListWindows(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; int pidFilter = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; std::string title = OptStr(args, "title"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_windows", (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseWindows(data, 936, all, perr)) return ParseErrorToJson(id, "window list", perr); auto pred = [&](const Json::Value& item) { if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; if (!MatchCI(item["title"].asString(), title)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个可见窗口(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "windows", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_network_state ---- static std::string BuildHostListNetworkState(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; int pidFilter = -1, localPort = -1, remotePort = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; if (!GetOptIntArg(id, args, "local_port", 0, 65535, localPort, errJson)) return errJson; if (!GetOptIntArg(id, args, "remote_port", 0, 65535, remotePort, errJson)) return errJson; std::string process = OptStr(args, "process"); std::string protocol = ToLowerAscii(OptStr(args, "protocol")); std::string state = OptStr(args, "state"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_network_state", (BYTE)COMMAND_MACHINE_NETSTATE, (BYTE)TOKEN_MACHINE_NETSTATE, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseNetState(data, 936, all, perr)) return ParseErrorToJson(id, "network state", perr); auto pred = [&](const Json::Value& item) { if (pidFilter >= 0 && item["pid"].asUInt() != (Json::UInt)pidFilter) return false; if (localPort >= 0 && item["localPort"].asInt() != localPort) return false; if (remotePort >= 0 && item["remotePort"].asInt() != remotePort) return false; if (!process.empty() && !MatchCI(item["process"].asString(), process)) return false; if (!protocol.empty() && protocol != "all" && item["protocol"].asString() != protocol) return false; if (!state.empty() && !MatchCI(item["state"].asString(), state)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条网络连接(全部 " + std::to_string(totalAll) + u8" 条)"; return BuildListResult(id, "connections", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_software ---- static std::string BuildHostListSoftware(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string name = OptStr(args, "name"); std::string publisher = OptStr(args, "publisher"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_software", (BYTE)COMMAND_MACHINE_SOFTWARE, (BYTE)TOKEN_MACHINE_SOFTWARE, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseSoftware(data, 936, all, perr)) return ParseErrorToJson(id, "software list", perr); auto pred = [&](const Json::Value& item) { if (!MatchCI(item["name"].asString(), name)) return false; if (!MatchCI(item["publisher"].asString(), publisher)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个已安装软件(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "software", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_browser_history ---- static std::string BuildHostListBrowserHistory(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string title = OptStr(args, "title"); std::string url = OptStr(args, "url"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_browser_history", (BYTE)COMMAND_MACHINE_HTML, (BYTE)TOKEN_MACHINE_HTML, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseBrowserHistory(data, 936, all, perr)) return ParseErrorToJson(id, "browser history", perr); auto pred = [&](const Json::Value& item) { if (!MatchCI(item["title"].asString(), title)) return false; if (!MatchCI(item["url"].asString(), url)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 条浏览记录(全部 " + std::to_string(totalAll) + u8" 条)"; return BuildListResult(id, "history", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_favorites ---- static std::string BuildHostListFavorites(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string name = OptStr(args, "name"); std::string url = OptStr(args, "url"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_favorites", (BYTE)COMMAND_MACHINE_FAVORITES, (BYTE)TOKEN_MACHINE_FAVORITES, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseFavorites(data, 936, all, perr)) return ParseErrorToJson(id, "favorites", perr); auto pred = [&](const Json::Value& item) { if (!MatchCI(item["name"].asString(), name)) return false; if (!MatchCI(item["url"].asString(), url)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个收藏(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "favorites", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_win32_services / host_list_driver_services ---- static std::string BuildHostListServices(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent, bool drivers) { HostTool ht; std::string errJson; const char* toolName = drivers ? "host_list_driver_services" : "host_list_win32_services"; BYTE tab = drivers ? (BYTE)COMMAND_MACHINE_DRIVERSERVICE : (BYTE)COMMAND_MACHINE_WIN32SERVICE; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string name = OptStr(args, "name"); std::string status = OptStr(args, "status"); std::string startType = OptStr(args, "start_type"); std::vector data; if (!FetchMachineTab(id, ht, toolName, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseServices(data, 936, all, perr)) return ParseErrorToJson(id, "service list", perr); auto pred = [&](const Json::Value& item) { if (!name.empty() && !(MatchCI(item["name"].asString(), name) || MatchCI(item["displayName"].asString(), name))) return false; if (!MatchCI(item["state"].asString(), status)) return false; if (!MatchCI(item["startType"].asString(), startType)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个服务(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "services", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_list_scheduled_tasks ---- static std::string BuildHostListScheduledTasks(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; Paging paging; if (!ParsePaging(id, args, paging, errJson)) return errJson; std::string name = OptStr(args, "name"); std::string folder = OptStr(args, "folder"); std::string status = OptStr(args, "status"); std::vector data; if (!FetchMachineTab(id, ht, "host_list_scheduled_tasks", (BYTE)COMMAND_MACHINE_TASK, (BYTE)TOKEN_MACHINE_TASKLIST, data, errJson)) return errJson; Json::Value all; std::string perr; if (!ParseTasks(data, all, perr)) return ParseErrorToJson(id, "scheduled task list", perr); auto pred = [&](const Json::Value& item) { if (!MatchCI(item["name"].asString(), name)) return false; if (!folder.empty() && !MatchCI(item["folder"].asString(), folder)) return false; if (!MatchCI(item["status"].asString(), status)) return false; return true; }; int totalAll = (int)all.size(), total = 0; bool hasMore = false; Json::Value page = FilterAndPage(all, pred, paging, total, hasMore); std::string text = std::string(u8"共 ") + std::to_string(total) + u8" 个计划任务(全部 " + std::to_string(totalAll) + u8" 个)"; return BuildListResult(id, "tasks", page, total, totalAll, paging.offset, paging.limit, hasMore, text); } // ---- host_get_hosts_file ---- static std::string BuildHostGetHostsFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { HostTool ht; std::string errJson; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; int maxBytes = 256 * 1024; if (!GetOptIntArg(id, args, "max_bytes", 1, 1024 * 1024, maxBytes, errJson)) return errJson; bool includeContent = GetBoolArg(args, "include_content", true); std::vector data; if (!FetchMachineTab(id, ht, "host_get_hosts_file", (BYTE)COMMAND_MACHINE_HOSTS, (BYTE)TOKEN_MACHINE_HOSTS, data, errJson)) return errJson; // [39][文件原文][2 个 0 字节]:取到第一个 NUL 为止。 if (data.size() < 1) return BuildError(id, kErrGeneric, "Empty hosts response"); size_t n = data.size() - 1; size_t strLen = BoundedStrlen((const char*)data.data() + 1, n); const BYTE* raw = data.data() + 1; UINT cp = 936; size_t skip = 0; if (strLen >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF) { cp = CP_UTF8; skip = 3; } else if (IsValidUtf8((const char*)raw, strLen)) { cp = CP_UTF8; } size_t contentLen = strLen - skip; bool truncated = contentLen > (size_t)maxBytes; size_t take = truncated ? (size_t)maxBytes : contentLen; std::string text = ToUtf8(std::string((const char*)raw + skip, take).c_str(), cp); Json::Value sc(Json::objectValue); sc["path"] = "%SystemRoot%\\System32\\drivers\\etc\\hosts"; sc["encoding"] = (cp == CP_UTF8) ? "utf-8" : "gbk"; sc["bytes"] = (Json::UInt)contentLen; sc["returnedBytes"] = (Json::UInt)take; sc["truncated"] = truncated; if (includeContent) { // 按行切分(去掉 \r,跳过末尾空行);截断时丢弃最后一行不完整的内容。 Json::Value lines(Json::arrayValue); size_t pos = 0; bool droppedPartial = false; while (pos < text.size()) { size_t nl = text.find('\n', pos); bool last = (nl == std::string::npos); std::string line = last ? text.substr(pos) : text.substr(pos, nl - pos); if (!line.empty() && line[line.size() - 1] == '\r') line.erase(line.size() - 1); if (truncated && last) { droppedPartial = true; break; } lines.append(line); if (last) break; pos = nl + 1; } if (droppedPartial) sc["truncatedAtLineBoundary"] = true; sc["lines"] = lines; sc["lineCount"] = (Json::UInt)lines.size(); if (!truncated) sc["content"] = text; } std::string summary = std::string(u8"hosts 文件 ") + std::to_string(contentLen) + u8" 字节" + (truncated ? u8"(已截断)" : ""); return BuildStructuredResult(id, sc, summary); } // ============================================================================ // 主机管理:写工具(只读 + 可恢复操作) // ============================================================================ namespace { bool RequireWritable(const Json::Value& id, std::string& errJson) { if (McpServer().IsReadonly()) { errJson = BuildError(id, kErrDisabled, "Host management write operations are disabled: MCP is in read-only mode " "(set McpReadonly=0 to enable)"); return false; } return true; } bool RequireDangerous(const Json::Value& id, std::string& errJson) { if (!McpServer().IsMachineDangerousEnabled()) { errJson = BuildError(id, kErrDisabled, "This action is disabled: set McpMachineDangerous=1 to enable it"); return false; } return true; } // 从 JSON 读 hwnd:接受整数、十进制字符串或 0x 前缀十六进制字符串。 bool ReadHwndArg(const Json::Value& id, const Json::Value& args, uint32_t& out, std::string& errJson) { if (!args.isMember("hwnd")) { errJson = BuildError(id, -32602, "Missing required parameter: hwnd"); return false; } const Json::Value& v = args["hwnd"]; if (v.isNumeric()) { out = (uint32_t)v.asUInt64(); return true; } if (v.isString()) { std::string s = Trim(v.asString()); if (s.size() > 2 && (s[0] == '0') && (s[1] == 'x' || s[1] == 'X')) { out = (uint32_t)strtoul(s.c_str() + 2, nullptr, 16); return true; } if (IsDigits(s)) { out = (uint32_t)strtoul(s.c_str(), nullptr, 10); return true; } } errJson = BuildError(id, -32602, "Invalid hwnd: expected an integer, decimal string or 0x-hex string"); return false; } const Json::Value* FindProcess(const Json::Value& list, DWORD pid) { for (Json::ArrayIndex i = 0; i < list.size(); ++i) { if (list[i]["pid"].asUInt() == (Json::UInt)pid) return &list[i]; } return nullptr; } const Json::Value* FindWindow(const Json::Value& list, DWORD hwnd) { for (Json::ArrayIndex i = 0; i < list.size(); ++i) { if (list[i]["hwnd"].asUInt() == (Json::UInt)hwnd) return &list[i]; } return nullptr; } const Json::Value* FindService(const Json::Value& list, const std::string& name) { for (Json::ArrayIndex i = 0; i < list.size(); ++i) { if (EqualsCI(list[i]["name"].asString(), name)) return &list[i]; } return nullptr; } bool ServiceStateIn(const std::string& state, std::initializer_list accepted) { for (const char* s : accepted) { if (EqualsCI(state, s)) return true; } return false; } // 服务 tab 先取列表(同时把客户端 m_dwServiceType 钉到正确类型)。 bool FetchServicesList(const Json::Value& id, ManagedSession& session, BYTE tab, Json::Value& all, std::string& errJson) { std::vector data; if (!session.Request(id, tab, (BYTE)TOKEN_MACHINE_SERVICE_LIST, data, errJson)) return false; std::string perr; if (!ParseServices(data, 936, all, perr)) { errJson = ParseErrorToJson(id, "service list", perr); return false; } return true; } } // namespace // ---- host_control_process(freeze / thaw) ---- static std::string BuildHostControlProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { std::string errJson; if (!RequireWritable(id, errJson)) return errJson; std::string action = ToLowerAscii(OptStr(args, "action")); if (action != "freeze" && action != "thaw") return BuildError(id, -32602, "Invalid action: expected freeze or thaw"); if (!RequireDangerous(id, errJson)) return errJson; int pidFilter = -1; if (!GetOptIntArg(id, args, "pid", 0, 0x7FFFFFFF, pidFilter, errJson)) return errJson; if (pidFilter < 0) return BuildError(id, -32602, "Missing required parameter: pid"); std::string expectedName = OptStr(args, "name"); HostTool ht; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; ManagedSession session; if (!session.Open(id, "host_control_process", ht, false, nullptr, errJson)) return errJson; std::vector data; if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS, data, errJson)) return errJson; Json::Value pre; std::string perr; if (!ParseProcesses(data, 936, pre, perr)) return ParseErrorToJson(id, "process list", perr); const Json::Value* proc = FindProcess(pre, (DWORD)pidFilter); if (!proc) { AuditManageAction(parent, ht.dev, "host_control_process", action + " pid=" + std::to_string(pidFilter) + " result=not_found"); return NotFoundJson(id, "process pid=" + std::to_string(pidFilter)); } if (!expectedName.empty() && !EqualsCI(proc->get("name", Json::Value("")).asString(), expectedName)) { // PID 可能已被系统复用:名字对不上时拒绝,避免误冻别的进程 return NotFoundJson(id, "process pid=" + std::to_string(pidFilter) + " named '" + expectedName + "' (current name: " + proc->get("name", Json::Value("")).asString() + ")"); } std::vector pkt(5); pkt[0] = (action == "freeze") ? (BYTE)COMMAND_PROCESS_FREEZING : (BYTE)COMMAND_PROCESS_THAW; DWORD pid32 = (DWORD)pidFilter; memcpy(&pkt[1], &pid32, 4); if (!session.SendNoReply(id, pkt, errJson)) { AuditManageAction(parent, ht.dev, "host_control_process", action + " pid=" + std::to_string(pidFilter) + " result=send_failed"); return errJson; } // 动作不回包:再取一次进程列表复核目标仍在。 std::vector after; if (!session.Request(id, (BYTE)COMMAND_MACHINE_PROCESS, (BYTE)TOKEN_MACHINE_PROCESS, after, errJson)) return errJson; Json::Value post; if (!ParseProcesses(after, 936, post, perr)) return ParseErrorToJson(id, "process list", perr); session.Finish(); const Json::Value* now = FindProcess(post, (DWORD)pidFilter); bool present = now != nullptr; AuditManageAction(parent, ht.dev, "host_control_process", action + " pid=" + std::to_string(pidFilter) + " result=" + (present ? "present" : "missing")); if (!present) { return BuildActionResult(id, false, true, false, action, "pid=" + std::to_string(pidFilter), "missing", "process_gone", u8"动作已下发,但复核时进程已不存在(可能已自行退出)。"); } // 冻结不改变「进程是否存在」,客户端也不回报结果:只能确认动作已发送且目标仍在。 return BuildActionResult(id, true, true, true, action, "pid=" + std::to_string(pidFilter), "present", "", u8"动作已下发,复核时进程仍在。注意:冻结/解冻需要客户端管理员权限," u8"若未生效只能通过目标进程的行为确认。"); } // ---- host_control_window(restore / hide / maximize / minimize) ---- static std::string BuildHostControlWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { std::string errJson; if (!RequireWritable(id, errJson)) return errJson; std::string action = ToLowerAscii(OptStr(args, "action")); DWORD sw = 0; if (action == "restore") sw = SW_RESTORE; else if (action == "hide") sw = SW_HIDE; else if (action == "maximize") sw = SW_SHOWMAXIMIZED; else if (action == "minimize") sw = SW_SHOWMINIMIZED; else return BuildError(id, -32602, "Invalid action: expected restore, hide, maximize or minimize"); uint32_t hwnd = 0; if (!ReadHwndArg(id, args, hwnd, errJson)) return errJson; HostTool ht; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; ManagedSession session; if (!session.Open(id, "host_control_window", ht, false, nullptr, errJson)) return errJson; std::vector data; if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS, data, errJson)) return errJson; Json::Value pre; std::string perr; if (!ParseWindows(data, 936, pre, perr)) return ParseErrorToJson(id, "window list", perr); if (!FindWindow(pre, hwnd)) { AuditManageAction(parent, ht.dev, "host_control_window", action + " hwnd=" + std::to_string(hwnd) + " result=not_found"); return NotFoundJson(id, "window hwnd=" + std::to_string(hwnd)); } // [11][hwnd][SW_*] + 零填充到 20 字节(与 MFC 下发格式一致;客户端只读前 9 字节) std::vector pkt(20, 0); pkt[0] = (BYTE)COMMAND_WINDOW_OPERATE; DWORD hwnd32 = (DWORD)hwnd; memcpy(&pkt[1], &hwnd32, 4); memcpy(&pkt[5], &sw, 4); if (!session.SendNoReply(id, pkt, errJson)) { AuditManageAction(parent, ht.dev, "host_control_window", action + " hwnd=" + std::to_string(hwnd) + " result=send_failed"); return errJson; } std::vector after; if (!session.Request(id, (BYTE)COMMAND_MACHINE_WINDOWS, (BYTE)TOKEN_MACHINE_WINDOWS, after, errJson)) return errJson; Json::Value post; if (!ParseWindows(after, 936, post, perr)) return ParseErrorToJson(id, "window list", perr); session.Finish(); bool present = FindWindow(post, hwnd) != nullptr; // 列表只含可见窗口:hide 后应消失;其余动作应仍然可见(最小化不改变可见性)。 bool expectedPresent = (action != "hide"); std::string state = present ? "visible" : "hidden"; AuditManageAction(parent, ht.dev, "host_control_window", action + " hwnd=" + std::to_string(hwnd) + " result=" + state); if (present != expectedPresent) { return BuildActionResult(id, false, true, false, action, "hwnd=" + std::to_string(hwnd), state, "unexpected_state", u8"动作已下发,但复核结果与预期不符(窗口可能已关闭," u8"或该操作需要更高权限)。"); } return BuildActionResult(id, true, true, true, action, "hwnd=" + std::to_string(hwnd), state, "", action == "hide" ? u8"窗口已隐藏(可见窗口列表中已消失)。" : u8"动作已生效,窗口仍在可见窗口列表中。"); } // ---- host_control_service(start / pause / continue / stop / restart) ---- static std::string BuildHostControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { std::string errJson; if (!RequireWritable(id, errJson)) return errJson; std::string action = ToLowerAscii(OptStr(args, "action")); bool dangerous = (action == "stop" || action == "restart"); if (action != "start" && action != "stop" && action != "pause" && action != "continue" && action != "restart") return BuildError(id, -32602, "Invalid action: expected start, stop, pause, continue or restart"); if (dangerous && !RequireDangerous(id, errJson)) return errJson; std::string serviceName = Trim(GetStringArg(args, "name")); if (serviceName.empty()) return BuildError(id, -32602, "Missing required parameter: name (service name)"); HostTool ht; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; std::string ansiName; if (!Utf8ToCpExact(serviceName, 936, ansiName)) { return BuildError(id, -32602, "Service name contains characters that cannot be represented in the " "client's ANSI code page (CP936)"); } ManagedSession session; if (!session.Open(id, "host_control_service", ht, false, nullptr, errJson)) return errJson; // prime:优先 Win32 服务,找不到再查驱动服务(同时钉住客户端 m_dwServiceType)。 Json::Value list; BYTE serviceTab = (BYTE)COMMAND_MACHINE_WIN32SERVICE; if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson; const Json::Value* svc = FindService(list, serviceName); if (!svc) { serviceTab = (BYTE)COMMAND_MACHINE_DRIVERSERVICE; if (!FetchServicesList(id, session, serviceTab, list, errJson)) return errJson; svc = FindService(list, serviceName); } if (!svc) { AuditManageAction(parent, ht.dev, "host_control_service", action + " " + serviceName + " result=not_found"); return NotFoundJson(id, "service '" + serviceName + "'"); } std::string initial = svc->get("state", Json::Value("")).asString(); // 客户端只在状态满足时才真正动作:先按同样的规则判断,避免把 no-op 报成成功。 auto stateIs = [](const std::string& s, const char* want) { return EqualsCI(s, want); }; if (action == "start" && stateIs(initial, "Running")) { session.Finish(); return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_running", u8"服务已是 Running,未下发动作。"); } if (action == "stop" && stateIs(initial, "Stopped")) { session.Finish(); return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_stopped", u8"服务已是 Stopped,未下发动作。"); } if (action == "pause" && stateIs(initial, "Paused")) { session.Finish(); return BuildActionResult(id, true, true, false, action, serviceName, initial, "already_paused", u8"服务已是 Paused,未下发动作。"); } auto sendServiceAction = [&](BYTE cmd, const char* label, std::vector& resp, std::string& err) -> bool { std::vector pkt; pkt.reserve(1 + ansiName.size() + 1); pkt.push_back(cmd); pkt.insert(pkt.end(), ansiName.begin(), ansiName.end()); pkt.push_back(0); return session.Action(id, pkt, (BYTE)TOKEN_MACHINE_SERVICE_LIST, resp, err); }; auto findState = [&](const std::vector& resp, std::string& state, std::string& err) -> bool { Json::Value after; if (!ParseServices(resp, 936, after, err)) return false; const Json::Value* now = FindService(after, serviceName); if (!now) return false; state = now->get("state", Json::Value("")).asString(); return true; }; std::vector resp; std::string state; if (action != "restart") { BYTE cmd = (action == "start") ? (BYTE)COMMAND_STARTSERVERICE : (action == "stop") ? (BYTE)COMMAND_STOPSERVERICE : (action == "pause") ? (BYTE)COMMAND_PAUSESERVERICE : (BYTE)COMMAND_CONTINUESERVERICE; if (!sendServiceAction(cmd, action.c_str(), resp, errJson)) { AuditManageAction(parent, ht.dev, "host_control_service", action + " " + serviceName + " result=send_or_timeout"); return errJson; } } else { // restart = stop → 等刷新 → start → 等刷新 if (!sendServiceAction((BYTE)COMMAND_STOPSERVERICE, "stop", resp, errJson)) { AuditManageAction(parent, ht.dev, "host_control_service", "restart " + serviceName + " result=stop_send_or_timeout"); return errJson; } std::string sErr; if (!findState(resp, state, sErr)) { return BuildError(id, kErrGeneric, "Service list after stop could not be parsed: " + sErr); } if (!sendServiceAction((BYTE)COMMAND_STARTSERVERICE, "start", resp, errJson)) { AuditManageAction(parent, ht.dev, "host_control_service", "restart " + serviceName + " (stop state=" + state + ") result=start_send_or_timeout"); return errJson; } } std::string sErr; bool listed = findState(resp, state, sErr); session.Finish(); if (!listed) { AuditManageAction(parent, ht.dev, "host_control_service", action + " " + serviceName + " result=missing_or_bad_list"); return BuildActionResult(id, false, true, true, action, serviceName, "missing", "service_missing_or_bad_refresh", u8"动作已下发,但刷新后的服务列表里找不到该服务或无法解析。"); } bool ok = false; if (action == "start" || action == "continue" || action == "restart") ok = ServiceStateIn(state, { "Running", "Starting", "Continue" }); else if (action == "stop") ok = ServiceStateIn(state, { "Stopped", "Stopping" }); else if (action == "pause") ok = ServiceStateIn(state, { "Paused", "Pausing" }); AuditManageAction(parent, ht.dev, "host_control_service", action + " " + serviceName + " initial=" + initial + " result=" + state + (ok ? " (ok)" : " (unexpected)")); if (!ok) { return BuildActionResult(id, false, true, true, action, serviceName, state, "unexpected_state", u8"动作已下发,但复核时服务状态不在预期集合内" u8"(可能需要管理员权限,或服务拒绝该控制请求)。"); } return BuildActionResult(id, true, true, true, action, serviceName, state, "", action == "restart" ? u8"已先停止再启动,复核状态符合预期。" : u8"复核时服务状态符合预期。"); } // ---- host_control_task(run / stop) ---- static std::string BuildHostControlTask(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) { std::string errJson; if (!RequireWritable(id, errJson)) return errJson; std::string action = ToLowerAscii(OptStr(args, "action")); if (action != "run" && action != "stop") return BuildError(id, -32602, "Invalid action: expected run or stop"); std::string taskName = Trim(GetStringArg(args, "name")); if (taskName.empty()) return BuildError(id, -32602, "Missing required parameter: name"); std::string wantedFolder = Trim(GetStringArg(args, "folder")); HostTool ht; if (!ResolveHost(id, args, parent, ht, true, errJson)) return errJson; ManagedSession session; if (!session.Open(id, "host_control_task", ht, false, nullptr, errJson)) return errJson; std::vector data; if (!session.Request(id, (BYTE)COMMAND_MACHINE_TASK, (BYTE)TOKEN_MACHINE_TASKLIST, data, errJson)) return errJson; Json::Value pre; std::string perr; if (!ParseTasks(data, pre, perr)) return ParseErrorToJson(id, "scheduled task list", perr); std::vector hits; for (Json::ArrayIndex i = 0; i < pre.size(); ++i) { if (!EqualsCI(pre[i]["name"].asString(), taskName)) continue; if (!wantedFolder.empty() && !EqualsCI(pre[i]["folder"].asString(), wantedFolder)) continue; hits.push_back(&pre[i]); } if (hits.empty()) { AuditManageAction(parent, ht.dev, "host_control_task", action + " " + taskName + " result=not_found"); return NotFoundJson(id, "scheduled task '" + taskName + "'"); } if (hits.size() > 1) { return BuildError(id, -32602, "Ambiguous task name: " + std::to_string(hits.size()) + " tasks share this name; specify folder"); } std::string folder = hits[0]->get("folder", Json::Value("\\")).asString(); if (folder.empty()) folder = "\\"; std::string ansiFolder, ansiName; if (!Utf8ToCpExact(folder, 936, ansiFolder) || !Utf8ToCpExact(taskName, 936, ansiName)) { return BuildError(id, -32602, "Task folder/name contains characters that cannot be represented in the " "client's ANSI code page (CP936)"); } std::vector pkt; pkt.push_back(action == "run" ? (BYTE)COMMAND_TASKSTART : (BYTE)COMMAND_TASKSTOP); pkt.insert(pkt.end(), ansiFolder.begin(), ansiFolder.end()); pkt.push_back(0); pkt.insert(pkt.end(), ansiName.begin(), ansiName.end()); pkt.push_back(0); std::vector resp; if (!session.Action(id, pkt, (BYTE)TOKEN_MACHINE_TASKLIST, resp, errJson)) { AuditManageAction(parent, ht.dev, "host_control_task", action + " " + taskName + " result=send_or_timeout"); return errJson; } Json::Value post; if (!ParseTasks(resp, post, perr)) { return BuildError(id, kErrGeneric, "Task list after action could not be parsed: " + perr); } session.Finish(); const Json::Value* after = nullptr; for (Json::ArrayIndex i = 0; i < post.size(); ++i) { if (EqualsCI(post[i]["name"].asString(), taskName) && EqualsCI(post[i]["folder"].asString(), folder)) { after = &post[i]; break; } } if (!after) { AuditManageAction(parent, ht.dev, "host_control_task", action + " " + taskName + " result=missing"); return BuildActionResult(id, false, true, true, action, taskName, "missing", "task_missing", u8"动作已下发,但复核时任务已不在列表中。"); } std::string state = after->get("status", Json::Value("")).asString(); AuditManageAction(parent, ht.dev, "host_control_task", action + " " + taskName + " result=" + state); // 任务动作没有回执:只能确认任务仍存在并报告当前状态,不推断是否真的运行成功。 return BuildActionResult(id, true, true, true, action, taskName, state, "", action == "run" ? u8"动作已下发;任务仍存在,当前状态见 state。" u8"(客户端无回执,若任务被禁用或凭据无效,Run 会失败且状态仍为原值。)" : u8"动作已下发;任务仍存在,当前状态见 state。"); } // ============================================================================ // 工具注册与分发 // ============================================================================ namespace McpMachine { namespace { void AddTool(Json::Value& tools, const char* name, const char* desc, const Json::Value& inputSchema, const Json::Value& outputSchema) { Json::Value tool(Json::objectValue); tool["name"] = name; tool["description"] = desc; // 调用方传 u8 字面量,保证 JSON 里是 UTF-8 tool["inputSchema"] = inputSchema; tool["outputSchema"] = outputSchema; tools.append(tool); } // 主机管理各 tab 的条目 schema Json::Value ProcessItemSchema() { Json::Value p(Json::objectValue); p["pid"] = PropInt(); p["arch"] = PropStr(u8"x86 / x64"); p["title"] = PropStr(u8"进程主窗口标题"); p["name"] = PropStr(u8"可执行文件名"); p["path"] = PropStr(); p["priorityClass"] = PropInt(u8"Windows 优先级类原始值"); p["priority"] = PropStr(u8"优先级名称"); p["threads"] = PropInt(); p["user"] = PropStr(); p["workingSetKb"] = PropInt(); p["fileSizeKb"] = PropInt(); return p; } Json::Value WindowItemSchema() { Json::Value p(Json::objectValue); p["hwnd"] = PropInt(u8"窗口句柄(十进制)"); p["title"] = PropStr(); p["pid"] = PropInt(); p["width"] = PropInt(); p["height"] = PropInt(); p["canSee"] = PropBool(u8"客户端是否可抓取该窗口画面"); return p; } Json::Value NetStateItemSchema() { Json::Value p(Json::objectValue); p["process"] = PropStr(); p["pid"] = PropInt(); p["protocol"] = PropStr(u8"tcp / udp"); p["localEndpoint"] = PropStr(u8"地址:端口"); p["localAddress"] = PropStr(); p["localPort"] = PropInt(); p["remoteEndpoint"] = PropStr(u8"地址:端口;UDP 无对端时为 *.*.*.*:*"); p["remoteAddress"] = PropStr(); p["remotePort"] = PropInt(); p["state"] = PropStr(u8"TCP 状态;UDP 为空"); return p; } Json::Value ConnectionItemSchema() { Json::Value p(Json::objectValue); p["protocol"] = PropStr(u8"tcp / udp"); p["family"] = PropStr(u8"ipv4 / ipv6"); p["pid"] = PropInt(); p["process"] = PropStr(); p["localAddress"] = PropStr(); p["localPort"] = PropInt(); p["remoteAddress"] = PropStr(); p["remotePort"] = PropInt(); p["state"] = PropInt(u8"MIB_TCP_STATE_* 原始值"); p["stateName"] = PropStr(u8"TCP 状态名;UDP 为空"); p["killable"] = PropBool(u8"是否可被 kill_network_connection 强制断开(仅 IPv4 TCP)"); return p; } Json::Value SoftwareItemSchema() { Json::Value p(Json::objectValue); p["name"] = PropStr(); p["publisher"] = PropStr(); p["version"] = PropStr(); p["installDate"] = PropStr(); p["uninstallString"] = PropStr(u8"卸载命令原文(本工具不执行)"); return p; } Json::Value HistoryItemSchema() { Json::Value p(Json::objectValue); p["time"] = PropStr(); p["title"] = PropStr(); p["url"] = PropStr(); return p; } Json::Value FavoriteItemSchema() { Json::Value p(Json::objectValue); p["name"] = PropStr(); p["url"] = PropStr(); return p; } Json::Value ServiceItemSchema() { Json::Value p(Json::objectValue); p["displayName"] = PropStr(); p["description"] = PropStr(); p["state"] = PropStr(u8"Running / Stopped / Paused / ..."); p["startType"] = PropStr(); p["login"] = PropStr(); p["interactive"] = PropBool(); p["name"] = PropStr(u8"服务名(host_control_service 的 name 参数)"); p["binaryPath"] = PropStr(); return p; } Json::Value TaskItemSchema() { Json::Value p(Json::objectValue); p["name"] = PropStr(); p["path"] = PropStr(); p["folder"] = PropStr(u8"任务所在文件夹(host_control_task 的 folder 参数)"); p["exe"] = PropStr(); p["status"] = PropStr(u8"Ready / Running / Disabled / ..."); p["lastRun"] = PropStr(u8"上次运行时间,未知为空"); p["nextRun"] = PropStr(u8"下次运行时间,未知为空"); return p; } // 读工具公共 input:id + 分页 Json::Value ReadInputSchema(Json::Value props) { props["id"] = HostIdProp(); AddPagingProps(props); return MakeSchema(props, { "id" }); } } // namespace bool Handles(const std::string& toolName) { static const char* kTools[] = { "list_network_connections", "kill_network_connection", "host_list_processes", "host_list_windows", "host_list_network_state", "host_list_software", "host_list_browser_history", "host_list_favorites", "host_list_win32_services", "host_list_driver_services", "host_list_scheduled_tasks", "host_get_hosts_file", "host_control_process", "host_control_window", "host_control_service", "host_control_task", }; for (const char* t : kTools) { if (toolName == t) return true; } return false; } void AppendTools(Json::Value& tools) { CMcpServer& mcp = McpServer(); // 总开关关闭:本族工具全部不可见(直接调用返回 -32006)。 if (!mcp.IsMachineEnabled()) return; const bool writable = !mcp.IsReadonly(); // ---- 连接管理 ---- { Json::Value props(Json::objectValue); props["protocol"] = PropStrEnum(u8"协议过滤,默认 all", { "tcp", "udp", "all" }); props["family"] = PropStrEnum(u8"地址族过滤,默认 all", { "ipv4", "ipv6", "all" }); props["pid"] = PropInt(u8"按进程 PID 过滤"); props["process"] = PropStr(u8"按进程名子串过滤(不区分大小写)"); props["local_port"] = PropInt(); props["remote_port"] = PropInt(); props["remote_address"] = PropStr(u8"按远端地址子串过滤"); props["state"] = PropStr(u8"按状态过滤:数字(MIB_TCP_STATE_*)或状态名子串,如 ESTABLISHED"); AddTool(tools, "list_network_connections", u8"列出指定在线 Windows 主机的 TCP/UDP 连接(相当于「连接管理」对话框)。返回四元组、所属进程与 TCP 状态;killable=true 表示该连接可被 kill_network_connection 断开。", ReadInputSchema(props), MakeListOutputSchema("connections", ConnectionItemSchema())); } if (writable && mcp.IsManageEnabled()) { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["protocol"] = PropStrEnum(u8"协议,默认 tcp(仅 IPv4 TCP 可断开)", { "tcp", "udp" }); props["local_address"] = PropStr(u8"本地 IPv4 地址,如 192.168.0.55"); props["local_port"] = PropInt(u8"本地端口(主机序)"); props["remote_address"] = PropStr(u8"远端 IPv4 地址"); props["remote_port"] = PropInt(u8"远端端口(主机序)"); props["pid"] = PropInt(u8"可选:同时校验所属进程 PID(多行匹配时用于消歧)"); props["dry_run"] = PropBool(u8"true 时只校验目标存在,不发断开命令,默认 false"); AddTool(tools, "kill_network_connection", u8"强制断开指定在线 Windows 主机上的一条 IPv4 TCP 连接(客户端调 SetTcpEntry DELETE_TCB)。必须提供完整四元组;目标不存在返回 -32009(请先刷新 list_network_connections)。断开后会用客户端自动刷新回来的列表复核,若连接仍在则 ok=false、reason=still_present(常见原因:客户端无管理员权限)。", MakeSchema(props, { "id", "local_address", "local_port", "remote_address", "remote_port" }), ActionOutputSchema()); } // ---- 主机管理:10 个只读标签页 ---- { Json::Value props(Json::objectValue); props["pid"] = PropInt(u8"按 PID 过滤"); props["name"] = PropStr(u8"按进程名子串过滤"); props["arch"] = PropStrEnum(u8"按架构过滤", { "x86", "x64" }); AddTool(tools, "host_list_processes", u8"列出在线 Windows 主机的进程(主机管理「进程」标签页)。返回 PID、架构、窗口标题、路径、优先级、线程数、用户、工作集与文件大小。", ReadInputSchema(props), MakeListOutputSchema("processes", ProcessItemSchema())); } { Json::Value props(Json::objectValue); props["pid"] = PropInt(u8"按所属进程 PID 过滤"); props["title"] = PropStr(u8"按窗口标题子串过滤"); AddTool(tools, "host_list_windows", u8"列出在线 Windows 主机上当前可见的顶层窗口(主机管理「窗口」标签页)。只包含可见窗口,故隐藏后窗口会从结果中消失。hwnd 可用于 host_control_window。", ReadInputSchema(props), MakeListOutputSchema("windows", WindowItemSchema())); } { Json::Value props(Json::objectValue); props["pid"] = PropInt(); props["process"] = PropStr(u8"按进程名子串过滤"); props["protocol"] = PropStrEnum(u8"协议过滤", { "tcp", "udp" }); props["local_port"] = PropInt(); props["remote_port"] = PropInt(); props["state"] = PropStr(u8"按状态子串过滤"); AddTool(tools, "host_list_network_state", u8"列出在线 Windows 主机的网络连接及其所属进程(主机管理「网络状态」标签页)。数据来源与「连接管理」不同(这里是进程视角、含 UDP 的无对端标记);仅支持 IPv4 展示。", ReadInputSchema(props), MakeListOutputSchema("connections", NetStateItemSchema())); } { Json::Value props(Json::objectValue); props["name"] = PropStr(u8"按软件名子串过滤"); props["publisher"] = PropStr(u8"按发布者子串过滤"); AddTool(tools, "host_list_software", u8"列出在线 Windows 主机已安装的软件(主机管理「软件」标签页,来自注册表 Uninstall 项)。只读;不提供卸载能力。", ReadInputSchema(props), MakeListOutputSchema("software", SoftwareItemSchema())); } { Json::Value props(Json::objectValue); props["title"] = PropStr(u8"按标题子串过滤"); props["url"] = PropStr(u8"按 URL 子串过滤"); AddTool(tools, "host_list_browser_history", u8"列出在线 Windows 主机的 IE 浏览历史(主机管理「浏览记录」标签页)。隐私数据,仅在显式开启主机管理时可见。", ReadInputSchema(props), MakeListOutputSchema("history", HistoryItemSchema())); } { Json::Value props(Json::objectValue); props["name"] = PropStr(u8"按收藏名子串过滤"); props["url"] = PropStr(u8"按 URL 子串过滤"); AddTool(tools, "host_list_favorites", u8"列出在线 Windows 主机的 IE 收藏夹(主机管理「收藏夹」标签页)。隐私数据,仅在显式开启主机管理时可见。", ReadInputSchema(props), MakeListOutputSchema("favorites", FavoriteItemSchema())); } { Json::Value props(Json::objectValue); props["name"] = PropStr(u8"按服务名或显示名子串过滤"); props["status"] = PropStr(u8"按状态子串过滤,如 Running"); props["start_type"] = PropStr(u8"按启动类型子串过滤,如 Auto"); AddTool(tools, "host_list_win32_services", u8"列出在线 Windows 主机的 Win32 服务(主机管理「服务」标签页之一)。返回显示名、描述、状态、启动类型、登录身份、服务名与可执行路径;服务名可用于 host_control_service。", ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema())); } { Json::Value props(Json::objectValue); props["name"] = PropStr(u8"按服务名或显示名子串过滤"); props["status"] = PropStr(u8"按状态子串过滤"); props["start_type"] = PropStr(u8"按启动类型子串过滤"); AddTool(tools, "host_list_driver_services", u8"列出在线 Windows 主机的驱动服务(主机管理「服务」标签页之一)。字段同 host_list_win32_services。", ReadInputSchema(props), MakeListOutputSchema("services", ServiceItemSchema())); } { Json::Value props(Json::objectValue); props["name"] = PropStr(u8"按任务名子串过滤"); props["folder"] = PropStr(u8"按任务文件夹子串过滤,如 \\Microsoft"); props["status"] = PropStr(u8"按状态子串过滤,如 Ready / Disabled"); AddTool(tools, "host_list_scheduled_tasks", u8"列出在线 Windows 主机的计划任务(主机管理「计划任务」标签页)。返回任务名、完整路径、所在文件夹、可执行文件、状态与上次/下次运行时间。任务名 + folder 可用于 host_control_task。", ReadInputSchema(props), MakeListOutputSchema("tasks", TaskItemSchema())); } { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["include_content"] = PropBool(u8"是否返回按行切分的内容,默认 true"); props["max_bytes"] = PropInt(u8"最多返回的原始字节数,默认 262144,上限 1048576"); Json::Value outProps(Json::objectValue); outProps["path"] = PropStr(); outProps["encoding"] = PropStr(u8"utf-8 / gbk(自动探测)"); outProps["bytes"] = PropInt(u8"文件原始字节数"); outProps["returnedBytes"] = PropInt(); outProps["truncated"] = PropBool(); outProps["lineCount"] = PropInt(); Json::Value lines(Json::objectValue); lines["type"] = "array"; lines["items"] = PropStr(); outProps["lines"] = lines; outProps["content"] = PropStr(u8"未截断时的完整文本"); Json::Value out(Json::objectValue); out["type"] = "object"; out["properties"] = outProps; AddTool(tools, "host_get_hosts_file", u8"读取在线 Windows 主机的 hosts 文件(主机管理「HOSTS」标签页),按行返回并自动识别 UTF-8/GBK。只读;不提供写入能力。", MakeSchema(props, { "id" }), out); } // ---- 主机管理:4 个写工具(只读 + 可恢复操作) ---- if (writable) { { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["action"] = PropStrEnum(u8"freeze=挂起进程,thaw=恢复进程", { "freeze", "thaw" }); props["pid"] = PropInt(u8"目标进程 PID"); props["name"] = PropStr(u8"可选:校验进程名,防止 PID 被系统复用后误操作"); AddTool(tools, "host_control_process", u8"挂起或恢复在线 Windows 主机上的一个进程(对应主机管理「进程」右键的冻结/解冻)。可能导致目标进程超时或崩溃,故归入危险操作:需要同时开启 McpMachineDangerous。动作没有回执,客户端也不回报结果;工具会在动作后重新拉取进程列表确认目标仍存在。", MakeSchema(props, { "id", "action", "pid" }), ActionOutputSchema()); } { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["action"] = PropStrEnum(u8"restore/hide/maximize/minimize", { "restore", "hide", "maximize", "minimize" }); Json::Value hwnd(Json::objectValue); Json::Value types(Json::arrayValue); types.append("integer"); types.append("string"); hwnd["type"] = types; hwnd["description"] = u8"窗口句柄,取 host_list_windows 的 hwnd;整数或 0x 前缀字符串"; props["hwnd"] = hwnd; AddTool(tools, "host_control_window", u8"对在线 Windows 主机的窗口执行可恢复操作:restore(还原)/hide(隐藏)/maximize(最大化)/minimize(最小化)。动作后重新枚举可见窗口复核:hide 后 hwnd 应消失,其余动作后仍应存在。不提供关闭窗口。", MakeSchema(props, { "id", "action", "hwnd" }), ActionOutputSchema()); } { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["action"] = PropStrEnum(u8"start/stop/pause/continue/restart", { "start", "stop", "pause", "continue", "restart" }); props["name"] = PropStr(u8"服务名(ServiceName,不是显示名),取 host_list_*_services 的 name"); AddTool(tools, "host_control_service", u8"控制在线 Windows 主机的服务:start/stop/pause/continue/restart。客户端对每次控制都会自动回一份刷新后的服务列表,工具据此复核状态并如实返回 ok;stop 与 restart 归入危险操作,需要同时开启 McpMachineDangerous。restart 先 stop 再 start。不提供删除服务。", MakeSchema(props, { "id", "action", "name" }), ActionOutputSchema()); } { Json::Value props(Json::objectValue); props["id"] = HostIdProp(); props["action"] = PropStrEnum(u8"run=立即运行,stop=停止正在运行的任务", { "run", "stop" }); props["name"] = PropStr(u8"任务名,取 host_list_scheduled_tasks 的 name"); props["folder"] = PropStr(u8"可选:任务所在文件夹;同名任务存在多个时必填"); AddTool(tools, "host_control_task", u8"运行或停止在线 Windows 主机的计划任务(主机管理「计划任务」右键)。客户端无回执,工具只复核任务仍存在并报告当前状态,不声称任务一定执行成功。不提供创建/删除任务。", MakeSchema(props, { "id", "action", "name" }), ActionOutputSchema()); } } } std::string Call(const Json::Value& id, const std::string& toolName, const Json::Value& args, CMy2015RemoteDlg* parent) { CMcpServer& mcp = McpServer(); if (!mcp.IsMachineEnabled()) { return BuildError(id, kErrDisabled, "Host management tools are disabled (set McpMachine=1 to enable)"); } if (!args.isObject()) { return BuildError(id, -32602, "Invalid params: arguments must be an object"); } // ---- 连接管理 ---- if (toolName == "list_network_connections") return BuildListNetworkConnections(id, args, parent); if (toolName == "kill_network_connection") return BuildKillNetworkConnection(id, args, parent); // ---- 主机管理:只读 ---- if (toolName == "host_list_processes") return BuildHostListProcesses(id, args, parent); if (toolName == "host_list_windows") return BuildHostListWindows(id, args, parent); if (toolName == "host_list_network_state") return BuildHostListNetworkState(id, args, parent); if (toolName == "host_list_software") return BuildHostListSoftware(id, args, parent); if (toolName == "host_list_browser_history") return BuildHostListBrowserHistory(id, args, parent); if (toolName == "host_list_favorites") return BuildHostListFavorites(id, args, parent); if (toolName == "host_list_win32_services") return BuildHostListServices(id, args, parent, /*drivers=*/false); if (toolName == "host_list_driver_services") return BuildHostListServices(id, args, parent, /*drivers=*/true); if (toolName == "host_list_scheduled_tasks") return BuildHostListScheduledTasks(id, args, parent); if (toolName == "host_get_hosts_file") return BuildHostGetHostsFile(id, args, parent); // ---- 主机管理:写(每个 builder 内部再校验只读/危险开关) ---- if (toolName == "host_control_process") return BuildHostControlProcess(id, args, parent); if (toolName == "host_control_window") return BuildHostControlWindow(id, args, parent); if (toolName == "host_control_service") return BuildHostControlService(id, args, parent); if (toolName == "host_control_task") return BuildHostControlTask(id, args, parent); return BuildError(id, -32601, "Unknown tool: " + toolName); } } // namespace McpMachine