Expose the online-list TLS column through list_online_hosts, search_hosts and get_host_detail. The value comes from the flag the client reports at login, so old clients always read false. Add list_network_connections and kill_network_connection on top of the connection manager sub-connection protocol. The kill path matches the original 110-byte record instead of rebuilding addresses, supports a dry run, refuses UDP/IPv6 targets, and verifies that the quadruple really disappeared instead of assuming success. Add a host_* family backed by the machine manager protocol: ten read tools covering every tab of the host management dialog, and four recoverable write tools for process freeze/thaw, window restore/hide/ maximize/minimize, service start/stop/pause/continue/restart, and scheduled task run/stop. Every write re-reads the snapshot before sending and re-checks afterwards, reporting -32009 for stale targets rather than acting blindly. The family is gated by McpMachine, the risky subset by McpMachineDangerous, and writes additionally require read-only mode to be off. Route managed sub-connections from MessageHandle and OfflineProc, keep the single-in-flight rule by sharing the pending table with a request kind, and move the shared JSON/lock/encoding helpers into McpInternal so both tool families use one implementation. Settings dialog, design docs and en_US/zh_TW/de_DE translations are updated accordingly. Co-Authored-By: deepseek-flash
2885 lines
120 KiB
C++
2885 lines
120 KiB
C++
// 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 <ws2tcpip.h>
|
||
#include <algorithm>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <string.h>
|
||
#include <functional>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
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<const char*> 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<const char*> 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:{ <arrayKey>: [...], 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 <typename Pred>
|
||
Json::Value FilterAndPage(const Json::Value& all, Pred pred, const Paging& p,
|
||
int& totalFiltered, bool& hasMore) {
|
||
std::vector<Json::Value> 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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<BYTE>& 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 去掉末尾 "\<name>";不匹配时退回最后一个反斜杠之前。
|
||
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<BYTE>& data,
|
||
std::vector<ConnRecord>& 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<BYTE>* 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<BYTE> 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<BYTE>& 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<BYTE>& pkt, BYTE respToken,
|
||
std::vector<BYTE>& 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<BYTE>& 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<std::mutex> 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<std::mutex> 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<BYTE>& out,
|
||
context*& subCtxOut,
|
||
int timeoutMs, std::string& clientError) {
|
||
std::unique_lock<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<ConnRecord>& records, std::string& errJson) {
|
||
ManagedSession session;
|
||
std::vector<BYTE> 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<ConnRecord> 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<ConnRecord> 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<BYTE> first;
|
||
if (!session.Open(id, toolName, ht, /*connMgr=*/true, &first, errJson)) return errJson;
|
||
std::vector<ConnRecord> 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<BYTE> pkt;
|
||
pkt.reserve(1 + kConnRecordSize);
|
||
pkt.push_back((BYTE)COMMAND_KILL_CONNECTION);
|
||
pkt.insert(pkt.end(), latest->raw, latest->raw + kConnRecordSize);
|
||
|
||
std::vector<BYTE> 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<ConnRecord> 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<BYTE>& 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<const char*> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE> 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<BYTE>& resp, std::string& err) -> bool {
|
||
std::vector<BYTE> 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<BYTE>& 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<BYTE> 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<BYTE> 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<const Json::Value*> 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<BYTE> 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<BYTE> 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
|