11 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-flash 38cd207f5d Feature: Add host management and connection management MCP tools
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
2026-10-21 20:01:41 +02:00
yuanyuanxianganddeepseek-flash 6c8336b497 Fix: Repair task actions and harden machine management
Scheduled task actions never worked end to end: the server built task
packets with Unicode-style lengths in a MultiByte build, the client had
no dispatch for the run/stop/delete/create commands, and the directory
column was derived by replacing every occurrence of the task name, which
misplaced tasks whose name also appeared in a parent folder. Packets are
now assembled with narrow strings, the four commands are routed to
RunOrStopTask/DelTask/CreateTask, and the folder is taken from the task
path suffix only. The task list reply is trimmed to its real length
instead of sending the whole 32 KB scratch buffer.

On the client side, thawing a process resumed a single thread, the
inject output path was copied from a narrow string using a wide-character
length, and an empty window list dereferenced a NULL buffer. The service
list leaked its enumeration block and one service handle per skipped
entry, and the description was copied without a bound. Service start and
stop now validate every handle, initialize the status structure, and stop
synchronously before the start that follows a restart request, which
previously always failed because the service was still in STOP_PENDING.
Deleting a process used to test only the low byte of the PID and leaked
the process handle on early exits, and a failed path conversion kept the
previous process image name. The IE history timestamp wrote minutes and
seconds in the wrong order, the favorites reader queried and closed an
uninitialized registry key, software enumeration never closed its subkeys,
and the task scheduler objects were not released after use.

The network state list now frees the module, snapshot, and extended
tables on every exit path, uses HeapFree for tables allocated on the
process heap, and no longer calls an export that may be missing. Growth
failures everywhere keep the original buffer instead of overwriting the
pointer, the hosts reader rejects an invalid file size, and the COM task
walk releases definitions and folders.

Co-Authored-By: deepseek-flash
2026-10-20 21:48:13 +02:00
yuanyuanxianganddeepseek-flash c208b7368e Fix: Format net-state addresses as narrow strings on the client
The client is a MultiByte build, so the destination buffers are char[]
and _T("%s:%u") expands to an ANSI format that expects a char pointer.
The code nevertheless converted inet_ntoa() output to WCHAR* first, and
the ANSI %s stopped at the zero byte that follows the first wide
character. Every address therefore collapsed to its leading digit, so
192.168.0.55:443 was shown as 1 and 0.0.0.0:80 as 0 in the local and
remote address columns of the network connections tab.

Write the address and port straight into the char buffers with
sprintf_s in all four enumeration paths (legacy and Vista APIs, TCP and
UDP), and drop the now unused char2wchar_t helper.

Co-Authored-By: deepseek-flash
2026-10-20 21:24:47 +02:00
yuanyuanxianganddeepseek-flash 15a50db0a7 Fix: Keep IPv4 address bytes in network order for connection list
iphlpapi stores the address in network byte order inside the memory of
the DWORD, it is not a host-order integer. For 192.168.0.55 the DWORD
value on x86 is 0x3700A8C0 while its bytes are C0 A8 00 37, so shifting
the value by 24/16/8/0 produced 37 00 A8 C0 and the dialog rendered
"55.0.168.192" for both the local and remote address.

Copy the DWORD with memcpy instead, which preserves the memory layout
that inet_ntop and SetTcpEntry both expect. GetAddr4 is changed the
same way, otherwise a forced disconnect would rebuild a wrong address
and miss the target connection.

Co-Authored-By: deepseek-flash
2026-10-20 21:15:45 +02:00
yuanyuanxianganddeepseek-flash ca35e6e26f Feature: Add menu icons for the advanced and disclaimer entries
Add four 16x16 bitmaps and wire them through the existing m_bmOnline
table. The three entries under the "Advanced" menu had no icon at all,
and the disclaimer entry shared the help question mark with "what is
this", so the two were indistinguishable.

- desktop quality levels: a monitor with three ascending level bars in
  the screen
- screen preview levels: a thumbnail card with a sun, a folded corner
  and layered hills
- file transfer speed: a half-circle gauge with a red needle
- disclaimer: a document with a folded corner and an amber exclamation
  badge, so it no longer looks like the help entry

Grow the bitmap array to 75 entries and reserve resource IDs 412-415.

Co-Authored-By: deepseek-flash
2026-10-20 21:13:05 +02:00
yuanyuanxiang 679136e73e Release v1.4.1 2026-10-20 15:00:35 +02:00
yuanyuanxiang b637c94e24 Feature: Allow user to select an installation location for client 2026-10-20 13:12:16 +02:00
yuanyuanxianganddeepseek-flash f76cd79bb0 Fix: Renew expired licenses from today and report the actual expiry
Preset renewal stored its target date as the old expiry plus the entered
days, while the authorization actually issued to the client is always
today plus a day count. For a license that had already expired, the
preset date was therefore in the past: SendPendingRenewal clamped the
day count to one, the client received a one-day license starting today,
but the renewal tip and the logs still printed the stale preset date.
The result was a message such as "renewed until 2026-05-30" while the
license really ran until tomorrow.

Presetting a renewal now bases the new date on today when the license
has already expired, and on the original expiry date otherwise, which
matches the one-day-per-day extension admins expect for live licenses.
The renewal dialog and the success prompt state which base was used, so
the computed date is never ambiguous. Renewal tips and both logging
paths (heartbeat-driven SendPendingRenewal and client-requested
GenerateRenewalInfo) now print the date that was actually issued, which
also corrects legacy preset records whose stored date has already
passed.

Co-Authored-By: deepseek-flash
2026-10-19 15:15:49 +02:00
yuanyuanxianganddeepseek-flash 39c8daa9b0 Fix: Keep the audit log open across WebSocket reconnects
Hiding the browser tab while the audit log was open closed the socket,
because the visibility handler treats only the remote screen, terminal
and wall pages as streaming pages. Returning to the tab reconnected the
socket, and the onopen restore path took its catch-all branch, which
navigated to the device list. The audit log therefore vanished whenever
the browser was minimized.

The catch-all branch now leaves the active page alone when it is the
audit log, and only refreshes the device list in the background. Every
other page keeps the previous behaviour: the login page advances with a
cached token, and the terminal page falls back to the device list
because its session died with the socket. Audit results already rendered
stay on screen, so the admin can keep reading without re-querying.

Co-Authored-By: deepseek-flash
2026-10-19 10:39:36 +02:00
yuanyuanxianganddeepseek-flash cd4f3675fd Improve: Replace native browser prompts with themed in-app dialogs
Native alert() and confirm() render their buttons in the browser or OS
locale, so on a German system prompts such as the Clear Screen Wall
confirmation showed German labels that disagreed with the rest of the
web UI. Both are now served by a single themed modal that reuses the
logout confirmation styling: an info variant with one OK button for
notices, and a danger variant with Cancel/OK for destructive actions.

The alert helper is fire-and-forget, while the confirm helper returns a
Promise, so the two call sites that need an answer (clearWall and
deleteUser) are now async. Dialog title and message are written with
textContent so device names, user names and server error strings can
never be interpreted as markup. A pending dialog is resolved as
cancelled when another one supersedes it, keeping callers from hanging,
and clicking the backdrop or pressing Escape closes the dialog. Remote
keyboard forwarding now ignores keys while a dialog is open.

Co-Authored-By: deepseek-flash
2026-10-19 10:27:09 +02:00
yuanyuanxianganddeepseek-flash bbb3d8c2fa Fix: Recover proxied client IPs and fit audit filters on phones
The MFC web server now resolves the real client address from the
WebSocket upgrade request when the TCP peer is loopback, so audit records
and login rate limiting stop reporting the reverse proxy's 127.0.0.1.
X-Forwarded-For is preferred over X-Real-IP because its last hop is the
address the local proxy appended; a client-supplied header list cannot
forge it. Forwarded headers are ignored for non-loopback peers so a
client connecting directly over the internet cannot spoof its identity
or rotate addresses to bypass throttling. FRP deployments can keep using
the existing Proxy Protocol v2 support instead.

Collapse the audit log search conditions behind a toolbar toggle that
starts closed and switches to an accent-colored "Hide filters" state
while the panel is open. On portrait phones the panel uses one control
per row, and From/To move their border and padding onto a wrapper so the
iOS WebKit datetime-local width miscalculation cannot overflow the
viewport.

Co-Authored-By: deepseek-flash
2026-10-19 10:05:58 +02:00
44 changed files with 4606 additions and 477 deletions

No files matched your search

+25
View File
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志
### v1.4.1 (2026.10.20)
**Web 运维控制台 & 安全加固 & 单向 TLS**
**新功能:**
- **Web 屏幕墙**:同一标签页以 1×1 ~ 5×5 布局同时监控多台设备,桌面 9 路 / 触控 4 路实时解码,超出预算暂停并自动晋升;点击 tile 进入单画面控制;同一设备可重复添加并共享一路流;MFC 与 Go 服务端同步支持(旧前端 / 旧服务端自动降级)
- **安全审计日志**:登录 / 授权拒绝 / WS 生命周期 / 屏幕与终端会话 / 剪贴板(仅字节数)/ 设备认证 / 管理变更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 条内存环形缓冲;Web 新增审计日志查询页(时间 / 事件 / 操作者 / IP / 设备 / 关键字 / 告警过滤 + 分页)
- **离线设备历史(Go)**:`devices.json` 持久化所有登录过的设备,设备列表可按需显示离线记录;管理员可删除记录或对在线设备执行远程卸载
- **单向 TLS 传输**:设备端口同一端口嗅探 ClientHello,TLS 1.2 客户端加密、旧明文客户端不受影响;MFC「扩展 > TLS证书」运行时配置;在线列表新增 TLS 列,右键可远程启用 / 取消(客户端下次启动生效)
- **Web 远程桌面增强**:Ctrl+C/X/V 双向文本剪贴板同步、手机「I」输入框(经剪贴板桥注入)、登录会话跨标签页共享、管理员 SSO 桥接
- **客户端安装位置选择**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **Go Web UI 白标**:`YAMA_WEB_BRAND_NAME` 替换产品名并移除安全提示与外链
- **安全加固**:每 IP 连接与登录限流、单连接入站缓冲上限、屏幕 / 终端子连接绑定源 IP、可选 ConnAuth HMAC 校验
- **克隆机 ID 去重**:V2 `client_id` 折叠主机名,撞 ID 时服务端下发 `COMMAND_REGEN_CLIENT_ID` 让客户端重算重连
**改进:**
- 文件传输速度改由 MasterSettings 下发(修复重复弹窗),传输对话框标题实时显示速度
- 客户端聊天关闭仅隐藏;日志右键新增白名单;MCP 勾选即时刷新;Web 原生 alert/confirm 全部替换为主题化应用内对话框
- 非中文系统统一按 GBK / UTF-8 解码(对话框 / 进程列表 / 文件列表 / 路径 / 地理位置 / 主机管理 / 剪贴板)
**Bug 修复:**
- 共享 / 转移主机后重启主控不重连;分享 / 转移权限误判
- 已过期授权续期从今天起算,提示与日志显示实际到期日
- V2 上传快捷目录未展开 `%USERPROFILE%`;无摄像头主机发送 0×0 画面信息;列切换残留像素;Go 设备端口日志刷屏;审计页最小化后跳回设备页
### v1.4.0 (2026.10.7)
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
+25
View File
@@ -378,6 +378,31 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog
### v1.4.1 (2026.10.20)
**Web operations console & security hardening & one-way TLS**
**New features:**
- **Web screen wall**: monitor several devices in one tab with 1x1 to 5x5 layouts; desktop decodes up to 9 live streams and touch devices 4, tiles over budget show Paused and are promoted automatically; click a tile for single-screen control; the same device can be added repeatedly and shares one stream; supported by both the MFC and Go servers (old frontends / old servers degrade automatically)
- **Security audit log**: login attempts, authorization denials, WebSocket lifecycle, screen / terminal sessions, clipboard (byte counts only), device authentication and admin mutations; the Go server persists to `logs/audit.log`, the MFC server keeps the newest 1000 events in an in-memory ring; new Audit Log page with time / event / actor / IP / device / keyword / warn filters and paging
- **Offline device history (Go)**: `devices.json` persists every device that has ever logged in, and the device list can show offline records; admins can delete a record or remotely uninstall an online device
- **One-way TLS transport**: the device port sniffs ClientHello on the same port, so TLS 1.2 clients are encrypted while legacy plaintext clients keep working; "Extensions > TLS Certificate" configures the MFC server at runtime; a TLS column and a per-client remote enable / disable action (effective on the client's next start)
- **Web remote desktop enhancements**: bidirectional Ctrl+C/X/V text clipboard sync, a phone "I" input box that injects text through the clipboard bridge, login sessions shared across tabs, and an admin-only SSO bridge
- **Client install location**: ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **White-label Go web UI**: `YAMA_WEB_BRAND_NAME` replaces the product name and removes the security notice and external links
- **Security hardening**: per-IP connection and login rate limits, a per-connection inbound buffer cap, source-IP binding for screen / terminal sub-connections, and optional ConnAuth HMAC verification
- **Clone ID deduplication**: the V2 `client_id` folds in the hostname; on a collision the server sends `COMMAND_REGEN_CLIENT_ID` so the client recomputes and reconnects
**Improvements:**
- File-transfer speed is delivered via MasterSettings (fixes duplicate dialogs), and the transfer dialog title shows live speed
- Closing the client chat now hides it; whitelist action in the log context menu; MCP checkmark refreshes immediately; native browser alert/confirm replaced with themed in-app dialogs
- GBK / UTF-8 decoding across non-Chinese systems (dialogs, process list, file lists, paths, geolocation, machine management, clipboard)
**Bug fixes:**
- Hosts did not reconnect after a master restart following share / transfer; share / transfer permission misjudgment
- Expired licenses now renew from today, and prompts and logs show the actually issued date
- V2 quick-folder uploads did not expand `%USERPROFILE%`; webcam-less hosts sent 0x0 bitmap info; stale pixels after column toggles; Go device-port log flood; audit page reset after minimizing the browser
### v1.4.0 (2026.10.7)
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
+25
View File
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌
### v1.4.1 (2026.10.20)
**Web 維運控制台 & 安全加固 & 單向 TLS**
**新功能:**
- **Web 螢幕牆**:同一分頁以 1×1 ~ 5×5 佈局同時監控多台裝置,桌面 9 路 / 觸控 4 路即時解碼,超出預算暫停並自動晉升;點擊 tile 進入單畫面控制;同一裝置可重複加入並共享一路流;MFC 與 Go 伺服端同步支援(舊前端 / 舊伺服端自動降級)
- **安全稽核日誌**:登入 / 授權拒絕 / WS 生命週期 / 螢幕與終端工作階段 / 剪貼簿(僅位元組數)/ 裝置認證 / 管理變更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 條記憶體環形緩衝;Web 新增稽核日誌查詢頁(時間 / 事件 / 操作者 / IP / 裝置 / 關鍵字 / 告警過濾 + 分頁)
- **離線裝置歷史(Go)**:`devices.json` 持久化所有登入過的裝置,裝置列表可依需顯示離線記錄;管理員可刪除記錄或對線上裝置執行遠端解除安裝
- **單向 TLS 傳輸**:裝置連接埠同一連接埠嗅探 ClientHello,TLS 1.2 用戶端加密、舊明文用戶端不受影響;MFC「擴充 > TLS憑證」執行時設定;線上列表新增 TLS 欄,右鍵可遠端啟用 / 取消(用戶端下次啟動生效)
- **Web 遠端桌面增強**:Ctrl+C/X/V 雙向文字剪貼簿同步、手機「I」輸入框(經剪貼簿橋注入)、登入工作階段跨分頁共享、管理員 SSO 橋接
- **用戶端安裝位置選擇**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **Go Web UI 白標**:`YAMA_WEB_BRAND_NAME` 取代產品名稱並移除安全提示與外部連結
- **安全加固**:每 IP 連線與登入限流、單連線入站緩衝上限、螢幕 / 終端子連線綁定來源 IP、可選 ConnAuth HMAC 校驗
- **複製機 ID 去重**:V2 `client_id` 納入主機名稱,撞 ID 時伺服端下發 `COMMAND_REGEN_CLIENT_ID` 讓用戶端重算重連
**改進:**
- 檔案傳輸速度改由 MasterSettings 下發(修復重複彈窗),傳輸對話框標題即時顯示速度
- 用戶端聊天關閉僅隱藏;日誌右鍵新增白名單;MCP 勾選即時重新整理;Web 原生 alert/confirm 全部替換為主題化應用內對話框
- 非中文系統統一按 GBK / UTF-8 解碼(對話框 / 處理程序列表 / 檔案列表 / 路徑 / 地理位置 / 主機管理 / 剪貼簿)
**Bug 修復:**
- 分享 / 轉移主機後重啟主控不重連;分享 / 轉移權限誤判
- 已過期授權續期從今天起算,提示與日誌顯示實際到期日
- V2 上傳快捷目錄未展開 `%USERPROFILE%`;無攝影機主機傳送 0×0 畫面資訊;欄切換殘留像素;Go 裝置連接埠日誌刷屏;稽核頁最小化後跳回裝置頁
### v1.4.0 (2026.10.7)
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
+1 -1
View File
@@ -283,7 +283,7 @@ int main(int argc, const char *argv[])
int r = RegisterStartup(
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf, g_SETTINGS.installFlag);
if (r <= 0) {
BOOL s = self_del();
if (!IsDebug) {
+8 -7
View File
@@ -60,19 +60,20 @@ static void ResolveProcName(DWORD pid, char* out, int outLen)
out[outLen - 1] = 0;
}
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是把网络序地址直接放在 DWORD 的**内存**里,
// 不是主机序数值(192.168.0.55 在 x86 上 DWORD 数值是 0x3700A8C0、内存字节是 C0 A8 00 37)。
// 所以只能按内存拷贝:这样得到的正是 inet_ntop 需要的网络序字节,也能原样拼回 SetTcpEntry 的 DWORD。
// 按数值移位会把字节序弄反(显示成 55.0.168.192)。
static void PutAddr4(uint8_t* out, DWORD addrNet)
{
out[0] = (uint8_t)(addrNet >> 24);
out[1] = (uint8_t)(addrNet >> 16);
out[2] = (uint8_t)(addrNet >> 8);
out[3] = (uint8_t)(addrNet);
memcpy(out, &addrNet, 4);
}
static DWORD GetAddr4(const uint8_t* addr)
{
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
DWORD addrNet = 0;
memcpy(&addrNet, addr, 4);
return addrNet;
}
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
+386 -117
View File
@@ -122,6 +122,23 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
case COMMAND_MACHINE_TASK:
GetRoot();
break;
// 计划任务动作:lpBuffer+1 起是 NUL 结尾的窄字符串(服务端按 MultiByte 组包)
case COMMAND_TASKSTART:
RunOrStopTask(lpBuffer + 1, TRUE);
GetRoot();
break;
case COMMAND_TASKSTOP:
RunOrStopTask(lpBuffer + 1, FALSE);
GetRoot();
break;
case COMMAND_TASKDEL:
DelTask(lpBuffer + 1);
GetRoot();
break;
case COMMAND_TASKCREAT:
CreateTask(lpBuffer + 1);
GetRoot();
break;
case COMMAND_MACHINE_HOSTS:
SendHostsList();
break;
@@ -190,8 +207,13 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
DebugPrivilege(SE_DEBUG_NAME, TRUE);
char strProcessName[MAX_PATH] = { 0 };
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *(LPDWORD)(lpBuffer + 1));
if (hProcess == NULL) {
DebugPrivilege(SE_DEBUG_NAME, FALSE);
break;
}
GetModuleFileNameEx(hProcess, NULL, strProcessName, MAX_PATH);
if (lpBuffer[1]) {
// PID 的第 2~4 字节非零时也要删除:只看 lpBuffer[1] 会把低字节为 0 的 PID 漏掉
if (*(LPDWORD)(lpBuffer + 1) != 0) {
char Tem_path[MAX_PATH];
char path_one[MAX_PATH];
char path_two[MAX_PATH];
@@ -218,10 +240,10 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
DeleteFile(path_one); //删除目录(目录)
Sleep(200);
if (GetFileAttributes(strProcessName) == -1) //检查文件不存在 退出
return;
break;
n++;
if (n >= 10) //无法删除 一段时间后退出
return;
break;
}
}
CloseHandle(hProcess);
@@ -282,11 +304,13 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
while (b) {
if (th32.th32OwnerProcessID == Pid) {
HANDLE oth = OpenThread(THREAD_ALL_ACCESS, FALSE, th32.th32ThreadID);
if (oth) {
::ResumeThread(oth);
CloseHandle(oth);
break;
}
::Thread32Next(hThreadSnap, &th32);
}
// 冻结挂起了全部线程,解冻也必须逐个恢复;这里不能 break,否则进程只会恢复一个线程
b = ::Thread32Next(hThreadSnap, &th32);
}
::CloseHandle(hThreadSnap);
}
@@ -554,7 +578,11 @@ LPBYTE CMachineManager::getProcessList()
pe32.dwSize = sizeof(PROCESSENTRY32);
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
if (lpBuffer == NULL) {
CloseHandle(hSnapshot);
DebugPrivilege(SE_DEBUG_NAME, FALSE);
return NULL;
}
lpBuffer[0] = TOKEN_MACHINE_PROCESS;
dwOffset = 1;
@@ -572,6 +600,8 @@ LPBYTE CMachineManager::getProcessList()
if ((pe32.th32ProcessID != 0) && (pe32.th32ProcessID != 4) && (pe32.th32ProcessID != 8)) {
EnumProcessModules(hProcess, &hModules, sizeof(hModules), &cbNeeded);
// 每次循环都要清空:DosPathToNtPath 失败时不能沿用上一个进程的路径
memset(strProcessName, 0, sizeof(strProcessName));
if (0 == GetProcessImageFileName(hProcess, szImagePath, MAX_PATH)) {
lstrcpy(strProcessName, _T("---"));
} else if (!DosPathToNtPath(szImagePath, strProcessName)) {
@@ -590,8 +620,15 @@ LPBYTE CMachineManager::getProcessList()
dwLength = sizeof(DWORD) * 5 + sizeof(bool) + MAX_PATH * sizeof(char) + lstrlen(pe32.szExeFile) * sizeof(char) +
lstrlen(strProcessName) * sizeof(char) + lstrlen(strProcessUser) * sizeof(char) + 6;
// 缓冲区太小,再重新分配下
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL) {
// 扩容失败时原缓冲区仍然有效,结束收集并返回已拿到的记录
CloseHandle(hProcess);
break;
}
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, &(pe32.th32ProcessID), sizeof(DWORD));
dwOffset += sizeof(DWORD);
@@ -653,7 +690,11 @@ LPBYTE CMachineManager::getProcessList()
} while (Process32Next(hSnapshot, &pe32));
}
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
{
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpBuffer = lpNew;
}
DebugPrivilege(SE_DEBUG_NAME, FALSE);
CloseHandle(hSnapshot);
@@ -687,6 +728,13 @@ LPBYTE CMachineManager::getWindowsList()
{
LPBYTE lpBuffer = NULL;
EnumWindows((WNDENUMPROC)EnumWindowsProc, (LPARAM)&lpBuffer);
if (lpBuffer == NULL) {
// 一个可见窗口都没有时回调不会分配缓冲区:仍要回一个只带令牌的包,
// 否则下面写 lpBuffer[0] 就是空指针写,客户端会崩溃。
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1);
if (lpBuffer == NULL)
return NULL;
}
lpBuffer[0] = TOKEN_MACHINE_WINDOWS;
return lpBuffer;
}
@@ -739,13 +787,18 @@ bool CALLBACK CMachineManager::EnumWindowsProc(HWND hwnd, LPARAM lParam)
::GetWindowText(hwnd, m_ibfo.strTitle, 1024);
if (lpBuffer == NULL) {
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1);
dwOffset = 1;
lpBuffer = (LPBYTE)LocalAlloc(LPTR, dwOffset + sizeof(WINDOWSINFO));
if (lpBuffer == NULL)
return true;
} else {
dwOffset = (DWORD)LocalSize(lpBuffer);
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset + sizeof(WINDOWSINFO), LMEM_ZEROINIT | LMEM_MOVEABLE);
// 分配失败时原缓冲区仍然有效,不能把指针覆盖成 NULL
if (lpNew == NULL)
return true;
lpBuffer = lpNew;
}
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset + sizeof(WINDOWSINFO), LMEM_ZEROINIT | LMEM_MOVEABLE);
} catch (...) {
return true;
}
@@ -853,8 +906,15 @@ LPBYTE CMachineManager::getSoftWareList()
// 缓冲区太小,再重新分配下
dwLength = lstrlen(regDisplayName) * sizeof(char) + lstrlen(regPublisher) * sizeof(char) + lstrlen(regDisplayVersion) *
sizeof(char) + lstrlen(regInstallData) * sizeof(char) + lstrlen(regUninstallString) * sizeof(char) + 11;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL) {
// 扩容失败时原缓冲区仍然有效,跳过这一条并结束枚举
RegCloseKey(hSubKey);
break;
}
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, regDisplayName, lstrlen(regDisplayName) * sizeof(char) + 2);
dwOffset += lstrlen(regDisplayName) * sizeof(char) + 2;
@@ -870,11 +930,13 @@ LPBYTE CMachineManager::getSoftWareList()
memcpy(lpBuffer + dwOffset, regUninstallString, lstrlen(regUninstallString) * sizeof(char) + 2);
dwOffset += lstrlen(regUninstallString) * sizeof(char) + 2;
RegCloseKey(hSubKey);
}
}
}
} else {
return FALSE; //打开键失败
LocalFree(lpBuffer);
return NULL; //打开键失败
}
RegCloseKey(hKey);
@@ -898,6 +960,7 @@ LPBYTE CMachineManager::getSoftWareList()
// 取ParentKeyName键值,判断是否是补丁信息, 是补丁信息键值为"OperatingSystem"
RegQueryValueEx(hSubKey, _T("ParentKeyName"), 0, &dwType, (LPBYTE)regBufferValue, &dwRegSize);
if (lstrlen(regDisplayName) == 0 || lstrcmp(regBufferValue, _T("OperatingSystem")) == 0) { //判断是否是补丁信息
RegCloseKey(hSubKey);
continue;
}
@@ -935,8 +998,15 @@ LPBYTE CMachineManager::getSoftWareList()
// 缓冲区太小,再重新分配下
dwLength = lstrlen(regDisplayName) * sizeof(char) + lstrlen(regPublisher) * sizeof(char) + lstrlen(regDisplayVersion) *
sizeof(char) + lstrlen(regInstallData) * sizeof(char) + lstrlen(regUninstallString) * sizeof(char) + 11;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL) {
// 扩容失败时原缓冲区仍然有效,跳过这一条并结束枚举
RegCloseKey(hSubKey);
break;
}
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, regDisplayName, lstrlen(regDisplayName) * sizeof(char) + 2);
dwOffset += lstrlen(regDisplayName) * sizeof(char) + 2;
@@ -952,15 +1022,23 @@ LPBYTE CMachineManager::getSoftWareList()
memcpy(lpBuffer + dwOffset, regUninstallString, lstrlen(regUninstallString) * sizeof(char) + 2);
dwOffset += lstrlen(regUninstallString) * sizeof(char) + 2;
RegCloseKey(hSubKey);
}
}
}
} else
return FALSE; //打开键失败
} else {
//打开键失败
LocalFree(lpBuffer);
return NULL;
}
RegCloseKey(hKey);
}
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
{
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpBuffer = lpNew;
}
return lpBuffer;
}
@@ -991,6 +1069,12 @@ LPBYTE CMachineManager::getIEHistoryList()
DWORD dwOffset = 1;
DWORD dwLength = 0;
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 10000);
if (lpBuffer == NULL) {
if (SUCCEEDED(hr) && puhs != NULL)
puhs->Release();
CoUninitialize();
return NULL;
}
lpBuffer[0] = TOKEN_MACHINE_HTML;
Browsinghistory m_Browsinghistory;
dwLength = sizeof(Browsinghistory);
@@ -1012,12 +1096,20 @@ LPBYTE CMachineManager::getIEHistoryList()
SYSTEMTIME st;
if (&su.ftLastVisited != NULL) {
FileTimeToSystemTime(&su.ftLastVisited, &st);
wsprintf(m_Browsinghistory.strTime, _T("%d-%d-%d %d:%d:%d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wSecond, st.wMinute);
wsprintf(m_Browsinghistory.strTime, _T("%d-%02d-%02d %02d:%02d:%02d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
} else {
wsprintf(m_Browsinghistory.strTime, _T("---"));
}
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL) {
// 扩容失败时原缓冲区仍然有效,停止收集,返回已拿到的记录
CoTaskMemFree(su.pwcsUrl);
CoTaskMemFree(su.pwcsTitle);
break;
}
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, &m_Browsinghistory, sizeof(Browsinghistory));
dwOffset += sizeof(Browsinghistory);
@@ -1028,11 +1120,15 @@ LPBYTE CMachineManager::getIEHistoryList()
su.pwcsTitle = nullptr;
}
pesu->Release();
}
puhs->Release();
}
}
CoUninitialize();
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
{
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpBuffer = lpNew;
}
return lpBuffer;
}
@@ -1053,15 +1149,22 @@ void CMachineManager::FindFavoritesUrl(char* searchfilename, PBYTE &lpFUBuffer,
char tmpPath[MAX_PATH] = { 0 };
DWORD dwFULength = 0;
lstrcat(favpath, searchfilename);
// 拼接要限长:收藏夹目录较深时 searchfilename 可能已接近 MAX_PATH
if (lstrlen(searchfilename) + 4 >= MAX_PATH)
return;
lstrcpyn(favpath, searchfilename, MAX_PATH - 4);
lstrcat(favpath, _T("\\*.*"));
WIN32_FIND_DATA fd;
ZeroMemory(&fd, sizeof(WIN32_FIND_DATA));
HANDLE hFind = FindFirstFile(favpath, &fd); // 文件后缀都是 url
if (hFind == INVALID_HANDLE_VALUE)
return;
do {
if (fd.cFileName[0] != _T('.')) {
if (lstrlen(searchfilename) + 1 + lstrlen(fd.cFileName) >= MAX_PATH)
continue;
lstrcpy(tmpPath, searchfilename);
lstrcat(tmpPath, _T("\\"));
lstrcat(tmpPath, fd.cFileName);
@@ -1074,8 +1177,13 @@ void CMachineManager::FindFavoritesUrl(char* searchfilename, PBYTE &lpFUBuffer,
dwFULength = lstrlen(buf) * sizeof(char) + lstrlen(fd.cFileName) * sizeof(char) + 4;
if (LocalSize(lpFUBuffer) < (dwFUOffset + dwFULength))
lpFUBuffer = (LPBYTE)LocalReAlloc(lpFUBuffer, (dwFUOffset + dwFULength) + 1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpFUBuffer) < (dwFUOffset + dwFULength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpFUBuffer, (dwFUOffset + dwFULength) + 1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
// 扩容失败时原缓冲区仍然有效:跳过这条记录,不能让指针变成 NULL
if (lpNew == NULL)
continue;
lpFUBuffer = lpNew;
}
memcpy(lpFUBuffer + dwFUOffset, fd.cFileName, lstrlen(fd.cFileName) * sizeof(char) + 2);
dwFUOffset += lstrlen(fd.cFileName) * sizeof(char) + 2;
@@ -1094,21 +1202,32 @@ LPBYTE CMachineManager::getFavoritesUrlList()
char favpath[MAX_PATH] = { 0 };
// 从注册表获取收藏夹所在位置
HKEY hKEY;
HKEY hKEY = NULL;
DWORD type = REG_SZ;
LPCTSTR path = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");
DWORD cbData = 80;
::RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKEY);
::RegQueryValueEx(hKEY, _T("Favorites"), NULL, &type, (LPBYTE)favpath, &cbData);
DWORD cbData = sizeof(favpath);
// RegOpenKeyEx 失败时 hKEY 是无效值,不能再拿它查值/关闭
if (::RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKEY) == ERROR_SUCCESS) {
if (::RegQueryValueEx(hKEY, _T("Favorites"), NULL, &type, (LPBYTE)favpath, &cbData) != ERROR_SUCCESS)
favpath[0] = 0;
::RegCloseKey(hKEY);
}
favpath[MAX_PATH - 1] = 0;
LPBYTE lpFUBuffer = (LPBYTE)LocalAlloc(LPTR, 10000);
if (lpFUBuffer == NULL)
return NULL;
lpFUBuffer[0] = TOKEN_MACHINE_FAVORITES;
int dwFUOffset = 1;
if (favpath[0] != 0)
FindFavoritesUrl(favpath, lpFUBuffer, dwFUOffset);
lpFUBuffer = (LPBYTE)LocalReAlloc(lpFUBuffer, dwFUOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
{
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpFUBuffer, dwFUOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpFUBuffer = lpNew;
}
return lpFUBuffer;
}
@@ -1141,15 +1260,30 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
DWORD dwSize = 1024 * 8;
sc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (sc == NULL) {
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return NULL;
}
//第一次调用来得到需要多大的内存区
EnumServicesStatus(sc, dwScType, SERVICE_STATE_ALL, lpServices, size, &size, &ret, NULL);
//申请需要的内存
lpServices = (LPENUM_SERVICE_STATUS)LocalAlloc(LPTR, size);
if (lpServices == NULL) {
CloseServiceHandle(sc);
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return NULL;
}
EnumServicesStatus(sc, dwScType, SERVICE_STATE_ALL, lpServices, size, &size, &ret, NULL);
// 申请Buffer内存
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024 * sizeof(char));
if (lpBuffer == NULL) {
LocalFree(lpServices);
CloseServiceHandle(sc);
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return NULL;
}
lpBuffer[0] = TOKEN_MACHINE_SERVICE_LIST;
dwOffset = 1;
@@ -1166,7 +1300,7 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
// 描述
memset(lpDescription, 0, sizeof(lpDescription));
if (((LPSERVICE_DESCRIPTION)szInfo)->lpDescription != NULL)
lstrcpy(lpDescription, ((LPSERVICE_DESCRIPTION)szInfo)->lpDescription);
lstrcpyn(lpDescription, ((LPSERVICE_DESCRIPTION)szInfo)->lpDescription, sizeof(lpDescription));
// 状态
memset(strState, 0, sizeof(strState));
@@ -1200,6 +1334,7 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
ZeroMemory(szInfo, dwSize);
dwSize = 1024 * 8;
if (!QueryServiceConfig(sh, (LPQUERY_SERVICE_CONFIG)szInfo, dwSize, &dwSize)) {
CloseServiceHandle(sh);
continue;
}
// 启动类型
@@ -1234,8 +1369,15 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
//==========================================================================================
// 缓冲区太小,再重新分配下
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
// 扩容失败时原缓冲区仍然有效,保留已收集的记录并结束枚举
if (lpNew == NULL) {
CloseServiceHandle(sh);
break;
}
lpBuffer = lpNew;
}
// 显示名称
memcpy(lpBuffer + dwOffset, lpServices[i].lpDisplayName, lstrlen(lpServices[i].lpDisplayName) * sizeof(char) + sizeof(char));
@@ -1277,8 +1419,14 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
}
CloseServiceHandle(sc);
LocalFree(lpServices);
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
{
// 收缩到实际长度;失败时原缓冲区仍有效,直接按原大小返回
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpBuffer = lpNew;
}
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return lpBuffer;
}
@@ -1303,7 +1451,16 @@ void CMachineManager::SendHostsList()
if (hFile == INVALID_HANDLE_VALUE)
return;
dwSize = GetFileSize(hFile, NULL);
// GetFileSize 失败返回 INVALID_FILE_SIZE:不加判断会按 0xFFFFFFFF 分配并读入
if (dwSize == INVALID_FILE_SIZE) {
CloseHandle(hFile);
return;
}
lpBuffer = (LPBYTE)LocalAlloc(LPTR, dwSize + 2);
if (lpBuffer == NULL) {
CloseHandle(hFile);
return;
}
if (!ReadFile(hFile, lpBuffer + 1, dwSize, &dwRead, NULL)) {
LocalFree(lpBuffer);
CloseHandle(hFile);
@@ -1320,46 +1477,63 @@ void CMachineManager::SendHostsList()
LPBYTE CMachineManager::getNetStateList()
{
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
if (lpBuffer == NULL)
return NULL;
lpBuffer[0] = TOKEN_MACHINE_NETSTATE;
DWORD dwOffset = 1;
DWORD dwLength = 0;
WCHAR* temp;
// 定义扩展函数指针
PFNAllocateAndGetTcpExTableFromStack pAllocateAndGetTcpExTableFromStack;
PFNAllocateAndGetUdpExTableFromStack pAllocateAndGetUdpExTableFromStack;
// 获取扩展函数的入口地址
HMODULE hModule = ::LoadLibrary(_T("iphlpapi.dll"));
pAllocateAndGetTcpExTableFromStack =
pAllocateAndGetTcpExTableFromStack = hModule == NULL ? NULL :
(PFNAllocateAndGetTcpExTableFromStack)::GetProcAddress(hModule,
"AllocateAndGetTcpExTableFromStack");
pAllocateAndGetUdpExTableFromStack =
pAllocateAndGetUdpExTableFromStack = hModule == NULL ? NULL :
(PFNAllocateAndGetUdpExTableFromStack)::GetProcAddress(hModule,
"AllocateAndGetUdpExTableFromStack");
if (pAllocateAndGetTcpExTableFromStack != NULL || pAllocateAndGetUdpExTableFromStack != NULL) {
// 两个导出函数要么都有(XP/2003)、要么都没有(Vista+):用 && 避免只匹配到一个时空指针调用
if (pAllocateAndGetTcpExTableFromStack != NULL && pAllocateAndGetUdpExTableFromStack != NULL) {
// 调用扩展函数,获取TCP扩展连接表和UDP扩展监听表
PMIB_TCPEXTABLE pTcpExTable;
PMIB_UDPEXTABLE pUdpExTable;
PMIB_TCPEXTABLE pTcpExTable = NULL;
PMIB_UDPEXTABLE pUdpExTable = NULL;
// pTcpExTable和pUdpExTable所指的缓冲区自动由扩展函数在进程堆中申请
if (pAllocateAndGetTcpExTableFromStack(&pTcpExTable, TRUE, GetProcessHeap(), 2, 2) != 0) {
if (pTcpExTable)
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return NULL;
}
if (pAllocateAndGetUdpExTableFromStack(&pUdpExTable, TRUE, GetProcessHeap(), 2, 2) != 0) {
if (pTcpExTable)
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
if (pUdpExTable)
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return NULL;
}
// 给系统内的所有进程拍一个快照
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
if (pTcpExTable)
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
if (pUdpExTable)
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return NULL;
}
char szLocalAddr[128];
char szRemoteAddr[128];
char szProcessName[128];
in_addr inadLocal, inadRemote;
char strState[128];
DWORD dwRemotePort = 0;
@@ -1419,24 +1593,26 @@ LPBYTE CMachineManager::getNetStateList()
// 远程IP地址
inadRemote.s_addr = pTcpExTable->table[i].dwRemoteAddr;
temp = char2wchar_t(inet_ntoa(inadLocal));
wsprintf(szLocalAddr, _T("%s:%u"), temp,
// 客户端是 MultiByte 工程,缓冲区是 char[]:这里必须用窄字符格式化,
// 不能先转成 WCHAR* 再交给 _T("%s")(ANSI 的 %s 只会读到首字符,地址显示成 0/1)。
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
ntohs((unsigned short)(0x0000FFFF & pTcpExTable->table[i].dwLocalPort)));
SAFE_DELETE_AR(temp);
temp = char2wchar_t(inet_ntoa(inadRemote));
wsprintf(szRemoteAddr, _T("%s:%u"), temp,
sprintf_s(szRemoteAddr, "%s:%u", inet_ntoa(inadRemote),
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
SAFE_DELETE_AR(temp);
// 打印出此入口的信息
char strProcessName[100] = { 0 };
const char* strType = _T("[TCP]");
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pTcpExTable->table[i].dwProcessId, szProcessName));
ProcessPidToName(hProcessSnap, pTcpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(strState) *
sizeof(char) + 12;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL)
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
@@ -1461,23 +1637,25 @@ LPBYTE CMachineManager::getNetStateList()
for (DWORD i = 0; i < pUdpExTable->dwNumEntries; ++i) {
// 本地IP地址
inadLocal.s_addr = pUdpExTable->table[i].dwLocalAddr;
temp = char2wchar_t(inet_ntoa(inadLocal));
wsprintf(szLocalAddr, _T("%s:%u"), temp,
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
SAFE_DELETE_AR(temp);
// 打印出此入口的信息
char strProcessName[100] = { 0 };
const char* strType = _T("[UDP]");
const char* szRemoteAddr = _T("*.*.*.*:*");
const char* szUDPState = _T(" ");
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, szProcessName));
ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(szUDPState) *
sizeof(char) + 12;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL)
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
@@ -1498,28 +1676,43 @@ LPBYTE CMachineManager::getNetStateList()
dwOffset += lstrlen(szUDPState) * sizeof(char) + 2;
}
::CloseHandle(hProcessSnap);
::LocalFree(pTcpExTable);
::LocalFree(pUdpExTable);
// 扩展表是扩展函数在 GetProcessHeap() 上用 HeapAlloc 申请的,应对应用 HeapFree 释放
if (pTcpExTable)
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
if (pUdpExTable)
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
::FreeLibrary(hModule);
} else {
char szLocalAddr[128];
char szRemoteAddr[128];
char szProcessName[128];
in_addr inadLocal, inadRemote;
char strState[128];
DWORD dwRemotePort = 0;
PMIB_TCPEXTABLE_VISTA pTcpTable_Vista;
_InternalGetTcpTable2 pGetTcpTable = (_InternalGetTcpTable2)GetProcAddress(hModule, "InternalGetTcpTable2");
if (pGetTcpTable == NULL)
PMIB_TCPEXTABLE_VISTA pTcpTable_Vista = NULL;
_InternalGetTcpTable2 pGetTcpTable = hModule == NULL ? NULL :
(_InternalGetTcpTable2)GetProcAddress(hModule, "InternalGetTcpTable2");
if (pGetTcpTable == NULL) {
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return 0;
}
if (pGetTcpTable(&pTcpTable_Vista, GetProcessHeap(), 1))
if (pGetTcpTable(&pTcpTable_Vista, GetProcessHeap(), 1)) {
if (pTcpTable_Vista)
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return 0;
}
// 给系统内的所有进程拍一个快照
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
if (pTcpTable_Vista)
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
::FreeLibrary(hModule);
LocalFree(lpBuffer);
return NULL;
}
@@ -1577,25 +1770,25 @@ LPBYTE CMachineManager::getNetStateList()
// 远程IP地址
inadRemote.s_addr = pTcpTable_Vista->table[i].dwRemoteAddr;
temp = char2wchar_t(inet_ntoa(inadLocal));
wsprintf(szLocalAddr, _T("%s:%u"), temp,
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
ntohs((unsigned short)(0x0000FFFF & pTcpTable_Vista->table[i].dwLocalPort)));
SAFE_DELETE_AR(temp);
temp = char2wchar_t(inet_ntoa(inadRemote));
wsprintf(szRemoteAddr, _T("%s:%u"), temp,
sprintf_s(szRemoteAddr, "%s:%u", inet_ntoa(inadRemote),
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
SAFE_DELETE_AR(temp);
// 打印出此入口的信息
char strProcessName[100] = { 0 };
const char* strType = _T("[TCP]");
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pTcpTable_Vista->table[i].dwProcessId, szProcessName));
ProcessPidToName(hProcessSnap, pTcpTable_Vista->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(strState) *
sizeof(char) + 12;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL)
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
@@ -1619,7 +1812,7 @@ LPBYTE CMachineManager::getNetStateList()
PMIB_UDPEXTABLE pUdpExTable = NULL;
// 表明为 Vista 或者 7 操作系统
PFNInternalGetUdpTableWithOwnerPid pInternalGetUdpTableWithOwnerPid;
pInternalGetUdpTableWithOwnerPid =
pInternalGetUdpTableWithOwnerPid = hModule == NULL ? NULL :
(PFNInternalGetUdpTableWithOwnerPid)GetProcAddress(hModule, "InternalGetUdpTableWithOwnerPid");
if (pInternalGetUdpTableWithOwnerPid != NULL) {
if (pInternalGetUdpTableWithOwnerPid(&pUdpExTable, GetProcessHeap(), 1)) {
@@ -1627,8 +1820,12 @@ LPBYTE CMachineManager::getNetStateList()
HeapFree(GetProcessHeap(), 0, pUdpExTable);
}
::CloseHandle(hProcessSnap);
if (pTcpTable_Vista)
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
FreeLibrary(hModule);
hModule = NULL;
LocalFree(lpBuffer);
return 0;
}
@@ -1637,23 +1834,25 @@ LPBYTE CMachineManager::getNetStateList()
for (DWORD i = 0; i < pUdpExTable->dwNumEntries; ++i) {
// 本地IP地址
inadLocal.s_addr = pUdpExTable->table[i].dwLocalAddr;
temp = char2wchar_t(inet_ntoa(inadLocal));
wsprintf(szLocalAddr, _T("%s:%u"), temp,
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
SAFE_DELETE_AR(temp);
// 打印出此入口的信息
char strProcessName[100] = { 0 };
const char* strType = _T("[UDP]");
const char* szRemoteAddr = _T("*.*.*.*:*");
const char* szUDPState = _T(" ");
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, szProcessName));
ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(szUDPState) *
sizeof(char) + 12;
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew == NULL)
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
lpBuffer = lpNew;
}
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
@@ -1673,26 +1872,42 @@ LPBYTE CMachineManager::getNetStateList()
memcpy(lpBuffer + dwOffset, szUDPState, lstrlen(szUDPState) * sizeof(char) + 2);
dwOffset += lstrlen(szUDPState) * sizeof(char) + 2;
}
}
if (pUdpExTable)
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
}
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
// Vista 分支的资源必须在这里释放:每次刷新网络列表都会走到这里
::CloseHandle(hProcessSnap);
if (pTcpTable_Vista)
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
::FreeLibrary(hModule);
}
{
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpBuffer = lpNew;
}
return lpBuffer;
}
// 将进程ID号(PID)转化为进程名称
char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName)
char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName, int nSize)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(processEntry);
if (ProcessName == NULL || nSize <= 0)
return ProcessName;
// 找不到的话,默认进程名为“???”
lstrcpy(ProcessName, _T("???"));
lstrcpyn(ProcessName, _T("???"), nSize);
if (!::Process32First(hProcessSnap, &processEntry))
return ProcessName;
do {
if (processEntry.th32ProcessID == ProcessId) { // 就是这个进程
lstrcpy(ProcessName, processEntry.szExeFile);
// szExeFile 最长 MAX_PATH,目标缓冲区可能只有 100 字节,必须限长拷贝
lstrcpyn(ProcessName, processEntry.szExeFile, nSize);
break;
}
} while (::Process32Next(hProcessSnap, &processEntry));
@@ -1700,16 +1915,6 @@ char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, ch
}
wchar_t* CMachineManager::char2wchar_t(char* cstr)
{
int len = MultiByteToWideChar(CP_ACP, 0, cstr, -1, NULL, 0);
wchar_t* wstr = new wchar_t[len];
memset(wstr, 0, len * sizeof(wchar_t));
MultiByteToWideChar(CP_ACP, 0, cstr, -1, wstr, len);
return wstr;
}
BOOL CMachineManager::EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable)
{
HANDLE hToken;
@@ -1733,18 +1938,21 @@ BOOL CMachineManager::EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable)
void CMachineManager::DeleteService(LPBYTE lpBuffer, UINT nSize)
{
EnablePrivilege(SE_DEBUG_NAME, TRUE);
SERVICE_STATUS status;
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS);
DWORD _err = GetLastError();
BOOL isSuccess = QueryServiceStatus(service, &status);
if (status.dwCurrentState != SERVICE_STOPPED) {
isSuccess = ControlService(service, SERVICE_CONTROL_STOP, &status);
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (scm == NULL) {
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return;
}
isSuccess = ::DeleteService(service);
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS);
if (service != NULL) {
SERVICE_STATUS status = { 0 };
if (QueryServiceStatus(service, &status) && status.dwCurrentState != SERVICE_STOPPED) {
ControlService(service, SERVICE_CONTROL_STOP, &status);
}
::DeleteService(service);
CloseServiceHandle(service);
}
CloseServiceHandle(scm);
// 稍稍Sleep下,防止出错
@@ -1757,25 +1965,55 @@ void CMachineManager::DeleteService(LPBYTE lpBuffer, UINT nSize)
void CMachineManager::MyControlService(LPBYTE lpBuffer, UINT nType)
{
EnablePrivilege(SE_DEBUG_NAME, TRUE);
SERVICE_STATUS status;
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); // 打开服务管理对象
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); // 打开服务管理对象
if (scm == NULL) {
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return;
}
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS); // 打开www服务
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS); // 打开服务
if (service == NULL) {
CloseServiceHandle(scm);
SendServicesList(m_dwServiceType);
EnablePrivilege(SE_DEBUG_NAME, FALSE);
return;
}
SERVICE_STATUS status = { 0 };
BOOL isSuccess = QueryServiceStatus(service, &status);
switch (nType) {
case 0:
case 0: {
if (status.dwCurrentState == SERVICE_STOP_PENDING) {
// 服务端“重启服务”是 STOP 后 100ms 就发 START:这里等服务真正停下来,
// 否则 StartService 会以服务仍在停止中失败,表现为“重启后服务没起来”。
for (int i = 0; i < 50 && status.dwCurrentState == SERVICE_STOP_PENDING; i++) {
Sleep(100);
if (!QueryServiceStatus(service, &status))
break;
}
}
if (status.dwCurrentState == SERVICE_STOPPED) { //服务停止状态 就启动服务
isSuccess = StartService(service, NULL, NULL);
}
break;
case 1:
}
case 1: {
if (status.dwCurrentState != SERVICE_STOPPED) {
if (status.dwCurrentState != SERVICE_STOP_PENDING)
isSuccess = ControlService(service, SERVICE_CONTROL_STOP, &status);
// 同步等待停止完成(最多 5 秒),让紧随其后的 START 能成功
if (isSuccess || status.dwCurrentState == SERVICE_STOP_PENDING) {
for (int i = 0; i < 50 && status.dwCurrentState != SERVICE_STOPPED; i++) {
Sleep(100);
if (!QueryServiceStatus(service, &status))
break;
}
}
}
break;
}
case 2:
if (status.dwCurrentState == SERVICE_RUNNING) {
isSuccess = ControlService(service, SERVICE_CONTROL_PAUSE, &status);
@@ -1838,7 +2076,10 @@ PBYTE CMachineManager::GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset
pRegisteredTask->get_Definition(&pDefinition);
if (pDefinition) {
// GetProgramPath 内部会 Release 传入的 ITaskDefinition(接管所有权),
// 这里不能再释放一次,否则是双重 Release,枚举任务时客户端直接崩溃。
GetProgramPath(pDefinition, &exepath);
pDefinition = NULL;
}
switch (taskState) {
@@ -1891,6 +2132,7 @@ PBYTE CMachineManager::GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offs
//获取文件夹个数
hr = pFolders->get_Count(&Count);
if (FAILED(hr)) {
pFolders->Release();
return 0;
}
@@ -1910,6 +2152,7 @@ PBYTE CMachineManager::GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offs
continue;
}
GetFolderAll(ispFolder, data, offset);
ispFolder->Release();
}
pFolders->Release();
@@ -1969,7 +2212,27 @@ PBYTE CMachineManager::GetRoot()
lpList[0] = TOKEN_MACHINE_TASKLIST;
int offset = 1;
GetFolderAll(pRootFolder, lpList, offset);
Send((LPBYTE)lpList, (UINT)LocalSize(lpList));
// 只发实际数据,不要把 32KB 预分配缓冲区整个发出去。
// 服务端解析靠每条记录末尾的两个 NUL 判断结束,所以非空列表要多带 2 字节结束标记;
// 没有任务时只发令牌 1 字节(长度为 1 时服务端循环直接结束)。
UINT dwSend = 1;
if (offset > 1) {
dwSend = (UINT)(offset + 2);
if ((UINT)LocalSize(lpList) < dwSend) {
PBYTE lpNew = (PBYTE)LocalReAlloc(lpList, dwSend, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (lpNew != NULL)
lpList = lpNew;
}
if ((UINT)LocalSize(lpList) >= dwSend) {
lpList[offset] = 0;
lpList[offset + 1] = 0;
} else {
// 扩不出结束标记时只回令牌,避免服务端越界读
dwSend = 1;
}
}
Send((LPBYTE)lpList, dwSend);
LocalFree(lpList);
}
pRootFolder->Release();
@@ -2033,15 +2296,18 @@ BOOL CMachineManager::CreateTask(LPBYTE lpBuffer)
//设置被结束后自动延迟10分钟运行
pSettings->put_StartWhenAvailable(VARIANT_TRUE);
//创建触发器
ITrigger* pTrigger;
ITrigger* pTrigger = NULL;
hr = pTriggers->Create(TASK_TRIGGER_LOGON, &pTrigger);
IAction* pAction;
IAction* pAction = NULL;
//设置执行
pActions->Create(TASK_ACTION_EXEC, &pAction);
if (pAction) {
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
//设置程序路径等信息
if (pExecAction)
pExecAction->put_Path(CharToBSTR(path));
}
//注册
hr = pRootFolder->RegisterTaskDefinition(CharToBSTR(taskname), pDefinition, TASK_CREATE_OR_UPDATE,
_variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(L""), &pRegisteredTask);
@@ -2162,10 +2428,13 @@ void CMachineManager::SaveData(const std::string& taskname, const std::string& p
sum = nameLEN + taskpathLEN + statusLEN + exeLEN + sizeof(DATE) * 2;
// 动态扩展缓冲区
if (LocalSize(lpList) < (offset + sum)) {
lpList = (BYTE*)LocalReAlloc(lpList, LocalSize(lpList)+4*1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
if (!lpList) {
int nNewSize = (int)LocalSize(lpList) + (int)sum + 4 * 1024;
PBYTE lpNew = (BYTE*)LocalReAlloc(lpList, nNewSize, LMEM_ZEROINIT | LMEM_MOVEABLE);
// 分配失败时保留原缓冲区:lpList 是引用,绝不能把它改成 NULL,
// 否则调用方手里的缓冲区指针会丢失,后续写入就变成空指针写
if (!lpNew)
return;
}
lpList = lpNew;
}
//拷贝数据
+1 -2
View File
@@ -34,8 +34,7 @@ private:
HWND GetHwndByPid(DWORD dwProcessID);
bool Is64BitOS();
bool Is64BitPorcess(DWORD dwProcessID);
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
wchar_t* char2wchar_t(char* cstr);
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName, int nSize);
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
void DeleteService(LPBYTE lpBuffer, UINT nSize);
void MyControlService(LPBYTE lpBuffer, UINT nType);
+2 -2
View File
@@ -103,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,4,0
FILEVERSION 1,0,4,1
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
@@ -121,7 +121,7 @@ BEGIN
BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.4.0"
VALUE "FileVersion", "1.0.4.1"
VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll"
BIN
View File
Binary file not shown.
+22 -4
View File
@@ -371,10 +371,28 @@ BOOL CreateDirectoryRecursively(const char* path)
return TRUE;
}
const char* GetInstallDirectory(const char * startupName)
const char* GetInstallDirectory(const char * startupName, int flag)
{
static char folder[MAX_PATH] = { 0 };
if (GetEnvironmentVariableA("ProgramData", folder, MAX_PATH) > 0) {
char env[32] = { 0 };
switch (flag) {
case 1:
strcpy(env, "ProgramFiles");
break;
case 2:
strcpy(env, "SystemRoot"); // Windows 目录,通常 C:\Windows
break;
case 3:
strcpy(env, "APPDATA"); // 当前用户的漫游应用数据目录,即 ...\AppData\Roaming
break;
case 4:
strcpy(env, "LOCALAPPDATA"); // 当前用户的本地应用数据目录,即 ...\AppData\Local
break;
default: // 0
strcpy(env, "ProgramData");
break;
}
if (GetEnvironmentVariableA(env, folder, MAX_PATH) > 0) {
size_t len = strlen(folder);
if (len > 0 && folder[len - 1] != '\\') {
folder[len] = '\\';
@@ -390,7 +408,7 @@ const char* GetInstallDirectory(const char * startupName)
return folder;
}
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log)
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag)
{
#ifdef _DEBUG
return 1;
@@ -401,7 +419,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
if (GetUserNameA(username, &size)) {
Mprintf("RegisterStartup is running with user: %s. Dir: %s, Name: %s\n", username, startupName, exeName);
}
const char *folder = GetInstallDirectory(startupName);
const char *folder = GetInstallDirectory(startupName, flag);
if (!folder) {
return -1;
}
+2 -2
View File
@@ -1,13 +1,13 @@
#pragma once
#include <stdbool.h>
const char* GetInstallDirectory(const char* startupName);
const char* GetInstallDirectory(const char* startupName, int flag);
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
// return > 0 means to continue running else terminate.
// runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log);
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag);
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
bool IsSystemInSession0();
+3 -2
View File
@@ -320,10 +320,11 @@ int main(int argc, const char *argv[])
int r = RegisterStartup(
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf);
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf, g_ConnectAddress.installFlag);
if (r <= 0) {
if (g_ConnectAddress.iStartup == Startup_DLL) {
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
g_ConnectAddress.installFlag);
if (!folder) {
return -1;
}
+2 -1
View File
@@ -855,7 +855,8 @@ public:
char installDir[32]; // 安装目录
char installName[32]; // 安装名称
char installDesc[136];// 安装描述
char szFututre[500]; // 保留字段
int installFlag; // 安装标记
char szFututre[496]; // 保留字段
public:
void SetType(int typ)
+3 -1
View File
@@ -147,7 +147,8 @@ MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法:
"online":{"type":"boolean"},
"group":{"type":"string"},
"screen":{"type":"string"},
"clientType":{"type":"string"}
"clientType":{"type":"string"},
"tls":{"type":"boolean"}
}
}
}
@@ -231,6 +232,7 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
- 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen`
- 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么)
- 连接凭据:`tls`(布尔,对应在线主机列表的 TLS 列;`true` 表示该客户端本轮以 TLS 模式登录,是**登录时的凭据**而非实时探测;老客户端/空值一律 `false`)
输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。
+273
View File
@@ -0,0 +1,273 @@
# MCP P8 设计:在线主机 TLS 字段 / 连接管理 / 主机管理
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3~P6 → `Mcp_Manage_Design.md`(P7)。
> P7 复用的是「系统/窗口/服务」对话框使用的 `COMMAND_SYSTEM`、`COMMAND_WSLIST`、
> `COMMAND_SERVICECONFIG` 链路;本轮要覆盖的「连接管理」与「主机管理」走的是**另一套**
> 命令(`COMMAND_CONNECTION` / `COMMAND_MACHINE_MANAGE`),必须在同一子连接上收发多步,
> 因此新增一层会话抽象,并为工具族增加独立开关。
>
> 安全门结论(已确认):新增独立 `host_*` 工具族;写入范围限定「只读 + 可恢复操作」;
> 开关分两层——`McpMachine`(工具族总开关)与 `McpMachineDangerous`(高风险子集),
> 均默认关闭,并与 `McpReadonly` 叠加。现有 27 个工具(含 P7)一律不改行为。
## 1. 范围
| 交付 | 内容 |
|---|---|
| TLS 字段 | `list_online_hosts` / `search_hosts` / `get_host_detail` 增加 `tls` |
| 连接管理 | `list_network_connections`、`kill_network_connection` |
| 主机管理读 | 10 个标签页各一个:`host_list_processes` … `host_get_hosts_file` |
| 主机管理写 | `host_control_process` / `host_control_window` / `host_control_service` / `host_control_task` |
| 配置 | 设置对话框两个复选框 + 启动接线(`McpMachine` / `McpMachineDangerous`) |
不做的(明确排除):kill 进程、关闭窗口、删除服务/任务、卸载软件、写 hosts、
创建任务、强制删除文件、代码注入。
## 2. TLS 字段
- `HostJson.cpp` 的 `BuildHostJson()` 末尾追加
`device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");`。
数据源是登录时写入的 `ONLINELIST_TLS`(`2015RemoteDlg.cpp` 收到 `RES_TLS=="1"` 时置 `"Y"`)。
- 语义:**登录凭据**,不是实时探测。老客户端不上报该值 → `false`。
- `BuildHostJson` 同时被 WebService 的设备列表复用,Web 端也会多出该字段(向后兼容)。
- `get_host_detail` 复用 `BuildHostItemSchema()`,自动带上。
## 3. 会话架构
两条新链路都需要「主连接发一条命令 → 客户端新建子连接 → 子连接回就绪包 →
在同一子连接上多次收发」,现有 `m_Pending` 只能等主连接上的单包。做法是给
`PendingRequest` 增加 `kind`,**复用同一张表** `m_Pending`(从而天然保证「单设备单飞行」),
而不是另开一套状态。
```cpp
enum class PendingKind { Generic, FollowUp, Machine, ConnMgr };
```
| 成员 | 用途 |
|---|---|
| `m_SubCtxToDevice` | 已绑定的子连接 → device;绑定的子连接上所有包都由 MCP 消费 |
| `m_AbsorbedSubCtx` | `CancelIO` 后仍可能到达的包:静默丢弃 |
| `m_OrphanByDevice` / `m_OrphanByPeer` | 5 秒墓碑,防止迟到的就绪包弹出 MFC 对话框 |
### 3.1 时序(工具线程)
```
BeginMachineSession(dev, tool, mainCtx) // 登记单飞行;忙 → -32003
→ 主连接 Send2Client(COMMAND_MACHINE_MANAGE)
→ 客户端开 CMachineManager 子连接,回 TOKEN_SYSINFOLIST(61)
→ OnManagedSubPacket 绑定子连接、保存首包、notify_all
WaitManagedPacket(dev, 61, …, 5000) // 就绪
每次读:
ArmManagedResponse(dev, 期望 token)
→ 子连接 Send2Client([tab 0..9]) / WaitSendIdle
→ WaitManagedPacket(dev, 期望 token, …, 12000)
EndManagedSession(dev, cancelIo=true) // 关闭子连接,不留长连接
```
整体 60 秒会话截止;每一步取 `min(step, deadline - now)`。所有超时返回 `-32001`。
### 3.2 子连接路由(`OnManagedSubPacket`)
在 `3.1` 的模型下,UI 线程收到的每个包先经过钩子
(`MessageHandle` 中终端拦截块之后、`switch (cmd)` 之前):
1. 已绑定 → 只保留 `expectedToken` 匹配的包;`TOKEN_MACHINE_MSG(42)` 存为
`clientError`(CP936→UTF-8);其余丢弃。已绑定的子连接上的一切包都归 MCP。
2. `m_AbsorbedSubCtx` 命中 → 丢弃。
3. 未绑定且 token 不是 `61` / `255` → 交回主 switch(MFC 行为不变)。
4. 未绑定且是 `61` / `255`:
- 该 context 就是主连接(`FindMainContext(parent, cid)`)→ 交回主 switch;
- 否则在同 device 的未绑定会话中按 `clientID` 精确绑定;
- 老客户端子连接 `clientID` 可能为 0 → 按 `peer` 兜底,**仅当同 peer 唯一**时才绑定;
- 都不命中:墓碑未过期 → 吸收 + `CancelIO` + 消费;否则交回主 switch
(用户手动打开对话框)。
### 3.3 必须同步调整的现有语义
- `IsPending()` 排除 `Machine` / `ConnMgr`:否则会话在飞时用户点「进程管理」,
`case TOKEN_PSLIST` 会把界面包抢走。
- `TakeMainResponse()` 顶部对 managed kind 直接返回。
- `OfflineProc` 在 `OnManageSubClosed` 之后、`RemoveFromHostList` **之前**调用
`OnManagedContextClosed`(否则连接池复用同一 context 指针后,新连接的包会被误吞)。
- 锁序:`parent->m_cs` → `m_PendingMutex`;持锁期间禁止 `Send2Client` / `CancelIO` /
`WaitSendIdle`。
### 3.4 健壮性
- UI 线程只做「有界拷贝 + 保存 + 唤醒」:单包上限 8 MiB,超限置 `clientError` 并 `CancelIO`。
- 解析全部在 MCP 工作线程上做;字符串一律用 `BoundedStrlen` / 显式偏移,
未对齐的多字节整数用 `memcpy`,禁止指针强转。
- 客户端部分列表用 `Send(buf, LocalSize(buf))` 发送,长度可能带出少量尾随 0 填充:
记录解析循环遇到「剩余全 0」即结束,定长记录列表忽略不足一条记录的尾部。
## 4. 连接管理
### 4.1 协议
- 主连接:`COMMAND_CONNECTION(253)`;客户端建 `CConnectionsManager` 子连接。
- 子连接回 `[255][u16 LE count][ConnectionInfo × count]`。
- `ConnectionInfo`(`common/commands.h`,`#pragma pack(1)`,110 字节,已 `static_assert`):
`proto(1)` / `af(1)` / `pid(4)` / `localAddr[16]` / `localPort(2,主机序)` /
`remoteAddr[16]` / `remotePort(2,主机序)` / `state(4)` / `procName[64](UTF-8)`。
地址为网络序;IPv4 取前 4 字节,`af == AF_INET(2)`,IPv6 为 `AF_INET6(23)`。
- 强制断开:子连接发 `[254][原始 110 字节记录]`;客户端 `SetTcpEntry(DELETE_TCB)`
后自动重发一次 255。
### 4.2 工具
| 工具 | 门控 | 说明 |
|---|---|---|
| `list_network_connections` | `McpMachine` | 过滤 `protocol/family/pid/process/local_port/remote_port/remote_address/state` + 分页;输出 `killable = (proto==1 && af==AF_INET)` |
| `kill_network_connection` | `McpMachine && McpManage && !McpReadonly` | 必填四元组;可选 `pid` 消歧、`dry_run` |
`kill_network_connection` 的流程刻意避免「重新拼地址」:
1. 取最新列表,**按原始 110 字节记录精确匹配**四元组(可选同时校验 PID);
2. 匹配 0 条 → `-32009`(调用方应刷新);匹配多条 → `-32602`(要求给 `pid`);
3. 目标不是 IPv4 TCP → `-32005`,不发包;
4. `dry_run` → 直接返回目标;
5. `Arm(255)` → 发 `[254][原记录]` → 等刷新列表 → 复核该四元组是否消失。
客户端 `SetTcpEntry` 的返回值被忽略且需要管理员权限,所以「断开后仍在」返回
`structuredContent.ok=false, reason="still_present"`,**绝不谎报成功**。
每次调用写审计:`tcp a:p->b:q pid=N result=…`。
## 5. 主机管理
### 5.1 协议
- 主连接:`COMMAND_MACHINE_MANAGE(162)` → 客户端 `CMachineManager` 子连接回
`TOKEN_SYSINFOLIST(61)`。
- 之后在同一子连接上发 **1 字节 tab**(0–9),响应 token 31–42。
- 动作 10–30;多数动作客户端会自动回一份刷新后的列表(服务 = 40,任务 = 41),
进程冻结/解冻(16/17)与窗口操作(11)**不回包**。
`MachineManager` 枚举(`common/commands.h`):请求 0 PROCESS / 1 WINDOWS / 2 NETSTATE /
3 SOFTWARE / 4 HTML / 5 FAVORITES / 6 WIN32SERVICE / 7 DRIVERSERVICE / 8 TASK / 9 HOSTS;
响应 31 PROCESS / 32 WINDOWS / 33 NETSTATE / 34 SOFTWARE / 35 HTML / 36 FAVORITES /
37 WIN32SERVICE / 38 DRIVERSERVICE / 39 HOSTS / 40 SERVICE_LIST / 41 TASKLIST / 42 MACHINE_MSG。
### 5.2 读工具
| 工具 | tab | 响应 | 主要过滤 |
|---|---|---|---|
| `host_list_processes` | 0 | 31 | `pid` / `name` / `arch` |
| `host_list_windows` | 1 | 32 | `pid` / `title` |
| `host_list_network_state` | 2 | 33 | `pid` / `process` / `protocol` / `local_port` / `remote_port` / `state` |
| `host_list_software` | 3 | 34 | `name` / `publisher` |
| `host_list_browser_history` | 4 | 35 | `title` / `url` |
| `host_list_favorites` | 5 | 36 | `name` / `url` |
| `host_list_win32_services` | 6 | 40 | `name` / `status` / `start_type` |
| `host_list_driver_services` | 7 | 40 | 同上 |
| `host_list_scheduled_tasks` | 8 | 41 | `name` / `folder` / `status` |
| `host_get_hosts_file` | 9 | 39 | `include_content` / `max_bytes` |
公共分页 `offset`(默认 0)/ `limit`(默认 100,上限 1000);输出
`total`(过滤后)/ `totalAll` / `offset` / `limit` / `hasMore`。
### 5.3 记录格式(与 `CMachineDlg::Show*` 逐字段对齐)
| 列表 | 格式 |
|---|---|
| 进程 31 | `DWORD pid`、`bool is64`、`title[260]`、`exe`+pad、`path`+pad、`DWORD priClass`、`DWORD threads`、`user`+pad、`DWORD workingSetKb`、`DWORD fileSizeKb`;CP936 |
| 窗口 32 | `WINDOWSINFO` 定长 1044 字节;**只含可见窗口**;空列表 = 1 字节 |
| 网络状态 33 | 进程名、`DWORD pid`、`[TCP]/[UDP]`、`ip:port`、远端、状态,各串 +2;UDP 远端 `*.*.*.*:*`、状态为空;按最后一个 `:` 拆地址 |
| 软件 34 | 5 个串各 +2:显示名、发布者、版本、安装日期、卸载命令 |
| 浏览记录 35 | 定长 2148:`time[100]` + `title[1024]` + `url[1024]` |
| 收藏夹 36 | 2 个串各 +2 |
| 服务 40 | 8 个串各 +1 NUL:显示名、描述、状态、启动类型、登录身份、交互式、**服务名**、可执行路径 |
| 计划任务 41 | `name/path/exe` 为 UTF-8、`status` 为 GBK,各 +1 NUL;两个 `DATE`;两个 0 字节结束;`folder` 由 `path` 去掉末尾 `"\<name>"` 派生 |
| hosts 39 | `[39][文件原文][0]`;有 BOM → UTF-8,否则合法 UTF-8 → UTF-8,否则 CP936 |
### 5.4 写工具
统一模式:**先取列表校验目标存在 → 发动作 → 再取列表复核**。目标不在最新快照里
返回 `-32009`,绝不盲发。
| 工具 | action | 额外门控 | 动作包 | 复核 |
|---|---|---|---|---|
| `host_control_process` | `freeze` / `thaw` | `McpMachineDangerous` | `[16/17][DWORD pid]`(无回包) | 重新取 tab 0:PID 仍在 |
| `host_control_window` | `restore/hide/maximize/minimize` | — | `[11][DWORD hwnd][DWORD SW_*]` + 零填充到 20 字节(客户端只读前 9 字节) | 重新取 tab 1:hide 后 hwnd 应消失,其余应仍在 |
| `host_control_service` | `start`/`pause`/`continue` | — | `[22/24/25][服务名 CP936][NUL]` | 客户端自动回 40 |
| `host_control_service` | `stop`/`restart` | `McpMachineDangerous` | stop = `[23]`;restart = stop → 等 40 → start | 等 40(12 s),状态在期望集合内 |
| `host_control_task` | `run` / `stop` | — | `[29/28][folder CP936][NUL][name CP936][NUL]` | 客户端自动回 41:任务仍在 + 当前状态 |
补充规则:
- 服务动作前先取一次服务列表(prime),既校验目标,也把客户端 `m_dwServiceType`
钉到正确类型(Win32 / 驱动)。
- 已是目标状态时不下发动作,返回 `ok=true, changed=false, reason=already_*`。
- 冻结/解冻、窗口动作、任务动作**没有回执**:`verified` 只表示「已用动作后的快照复核」,
不表示「动作一定生效」。冻结不改变进程存在性,因此只能确认目标仍在。
- 写工具输入串(服务名、任务 folder/name)必须能转成 CP936,否则 `-32602`,不发包。
- 审计格式:`host <id> <tool>: action=… target=… result=…`。
## 6. 门控矩阵
| 配置 | 可见工具 | 直接调用被隐藏的工具 |
|---|---|---|
| `McpMachine=0`(默认) | 0 个 | `-32006` |
| `McpMachine=1, McpReadonly=1` | 11 个读工具(`list_network_connections` + 10 个 `host_list_*` / `host_get_hosts_file`) | 5 个写工具 → `-32006` |
| `McpMachine=1, McpReadonly=0, McpManage=1` | 16 个(含 5 个写) | — |
| `McpMachine=1, McpReadonly=0, McpManage=0` | 15 个(无 `kill_network_connection`) | `kill_network_connection` → `-32006` |
| `McpMachineDangerous=0` | 工具仍可见 | `host_control_process`、`host_control_service{stop,restart}` → `-32006` |
设置对话框勾选「危险主机操作」但未勾「主机管理」时,保存会清除危险开关并提示。
## 7. 错误码
沿用 P1–P7 的约定,新增 `-32009`:
| 码 | 含义 |
|---|---|
| `-32602` | 参数非法(含:目标多条匹配、字符无法用 CP936 表示) |
| `-32000` | 解析失败 / 客户端返回错误文本(`TOKEN_MACHINE_MSG`) |
| `-32001` | 超时(就绪 5 s / 列表 12 s / 会话 60 s) |
| `-32002` | 主机离线 |
| `-32003` | 设备忙(已有 MCP 会话在飞:单设备单飞行) |
| `-32004` | 发送失败 |
| `-32005` | 不支持(非 Windows 客户端;UDP/IPv6 断连) |
| `-32006` | 开关未开启 |
| `-32009` | **目标在最新快照中不存在**(调用方应刷新后重试) |
## 8. 已知限制
1. **token 数值碰撞**:机器响应 31–42 与 `COMMAND_KEYBOARD/AUDIO/SYSTEM/SHELL/…` 同值。
靠「先按子连接身份路由」解决——未绑定的子连接不会按 token 猜测。
2. **UI 争抢就绪包**:用户恰好在 MCP 请求期间点开同一对话框时,首包可能被 MCP 绑定,
界面对话框这次不弹(与 P7 的取舍一致),重试即可。
3. **5 s 墓碑**:MCP 异常结束后 5 s 内手动打开同一台主机的对话框可能被吞,重试即可。
4. **老客户端子连接 `clientID=0`**:靠 peer 兜底,仅在同 peer 唯一会话时绑定。
5. **权限**:`SetTcpEntry`、冻结/解冻、服务控制需要客户端管理员;失败按「未生效」如实返回。
6. **编码**:机器协议以 CP936 为主;任务 `name/path/exe` 为 UTF-8、`status` 为 GBK;
写回输入统一转 CP936。GBK 不可表示的字符明确报 `-32602`。
7. **`host_list_network_state` 只有 IPv4**:数据来自客户端自身的 `GetTcpTable` 视图,
与「连接管理」(`ConnectionInfo`,含 IPv6)不是同一份数据。
8. **`McpMachineDangerous` 只覆盖本轮高风险动作**;后续加入 kill/删除/卸载/注入时,
必须按 `McpMachine && McpMachineDangerous && !McpReadonly` 三层校验。
9. **`host_control_window {action:"hide"}` 是单向的**:客户端只枚举可见窗口,隐藏后该 hwnd
从快照中消失,再发 `restore` 会在发包前被 `-32009` 拦下(MFC 对话框同样无法再操作它)。
实测确认:`hide` 复核为 `state:"hidden"`,随后 `restore` 返回 `-32009`。
10. **`host_control_service {action:"pause"}` 依赖服务自身支持 PAUSE**:实测虚拟机上的
`Spooler` 下发 PAUSE 后状态仍为 `Running`,工具如实返回 `ok:false, reason:"unexpected_state"`
(不谎报成功)。`start/stop/continue/restart` 在同一台机器上均通过。
11. **开关只在进程启动时读取**(Release 版配置来自注册表 `HKCU\Software\YAMA\settings`,
不是 `Bin\settings.ini`);在「MCP设置」里改动 `McpMachine` / `McpMachineDangerous` /
`McpReadonly` 等必须重启服务端才生效。未重启时 `tools/list` 里看不到新工具,
直接调用返回 `-32006`。
## 9. 验证要点
- TLS:`RES_TLS=1` 的主机三个工具都返回 `tls:true`;老客户端 `false`。
- 路由:MCP 关闭时手动打开「主机管理」「连接管理」照常工作;
`host_list_favorites`(token 36,与键盘等命令同值)在飞时手动打开「进程管理」,
两边都不串包。
- 迟到包:强制就绪超时后,61/255 到达应被吸收且不弹对话框。
- 单飞行:同主机并发两个 host/connection 工具,第二个 `-32003`;两台主机并发都成功。
- 连接管理:与对话框逐条对比;断开后复核消失;UDP/IPv6 → `-32005`;过期四元组 → `-32009`。
- 10 个读工具:与 MFC 标签页对比行数、首末行、中文编码;空 tab 返回 `total:0`。
- 4 个写工具:freeze→thaw、窗口四动作、服务五动作、任务 run/stop;过期目标发包前 `-32009`。
- 回归:现有 27 个工具、终端、远程控制、文件传输、10 个 MFC 标签页行为不变。
- 资源:连续调用 20 次 host 工具,服务端无残留子连接。
Binary file not shown.
+42 -8
View File
@@ -742,6 +742,10 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[68].LoadBitmap(IDB_BITMAP_DRAWING_BOARD); // 远程画板
m_bmOnline[69].LoadBitmap(IDB_BITMAP_TLS_ENABLE); // TLS 传输
m_bmOnline[70].LoadBitmap(IDB_BITMAP_TLS_DISABLE); // 取消 TLS
m_bmOnline[71].LoadBitmap(IDB_BITMAP_DESKTOP_QUALITY); // 高级→远程桌面质量等级
m_bmOnline[72].LoadBitmap(IDB_BITMAP_PREVIEW_QUALITY); // 高级→屏幕预览质量等级
m_bmOnline[73].LoadBitmap(IDB_BITMAP_FILE_SPEED); // 高级→文件传输速度
m_bmOnline[74].LoadBitmap(IDB_BITMAP_DISCLAIMER); // 其他→免责声明
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
m_ServerDLL[i] = nullptr;
m_ServerBin[i] = nullptr;
@@ -1219,13 +1223,17 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
m_MainMenu.SetMenuItemBitmaps(ID_FRPS_FOR_SUB, MF_BYCOMMAND, &m_bmOnline[45], &m_bmOnline[45]);
m_MainMenu.SetMenuItemBitmaps(ID_WEB_REMOTE_CONTROL, MF_BYCOMMAND, &m_bmOnline[52], &m_bmOnline[52]);
// Help menu
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[74], &m_bmOnline[74]);
m_MainMenu.SetMenuItemBitmaps(ID_HELP_FEEDBACK, MF_BYCOMMAND, &m_bmOnline[47], &m_bmOnline[47]);
m_MainMenu.SetMenuItemBitmaps(ID_WHAT_IS_THIS, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]);
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_REQUEST_AUTH, MF_BYCOMMAND, &m_bmOnline[49], &m_bmOnline[49]);
m_MainMenu.SetMenuItemBitmaps(ID_UNINSTALL_SOFTWARE, MF_BYCOMMAND, &m_bmOnline[57], &m_bmOnline[57]);
m_MainMenu.SetMenuItemBitmaps(ID_ENABLE_DEV_DEBUG, MF_BYCOMMAND, &m_bmOnline[60], &m_bmOnline[60]);
// Advanced menu
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_QUALITY_LEVELS, MF_BYCOMMAND, &m_bmOnline[71], &m_bmOnline[71]);
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_PREVIEW_LEVELS, MF_BYCOMMAND, &m_bmOnline[72], &m_bmOnline[72]);
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_FILE_SPEED, MF_BYCOMMAND, &m_bmOnline[73], &m_bmOnline[73]);
// ============================================================
// UIBranding: 根据编译时配置隐藏菜单项
@@ -2410,6 +2418,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
// P8 主机管理 / 连接管理工具族:总开关默认关(关闭时 16 个工具都不可见)。
// 危险子集(进程冻结/解冻、服务停止/重启)在总开关之上再叠一道,默认关。
McpServer().SetMachineEnabled(THIS_CFG.GetInt("settings", "McpMachine", 0) != 0);
McpServer().SetMachineDangerousEnabled(
THIS_CFG.GetInt("settings", "McpMachineDangerous", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
@@ -5508,6 +5521,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
McpServer().OnManageSubClosed(ContextObject);
}
// MCP P8 主机管理 / 连接管理会话的子连接断开:清路由表、唤醒等待线程。必须在
// RemoveFromHostList 之前,否则连接池复用同一 context 指针后新连接的包会被误吞。
if (McpServer().IsRunning()) {
McpServer().OnManagedContextClosed(ContextObject);
}
SOCKET nSocket = ContextObject->sClientSocket;
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
@@ -5870,6 +5889,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
return;
}
// ===== MCP 主机管理 / 连接管理:子连接路由(P8)=====
// 已绑定的子连接上的所有包由 MCP 消费;未绑定的子连接只有就绪包
// TOKEN_SYSINFOLIST(61) / TOKEN_CONNECTION_LIST(255) 会被尝试绑定,其余原样交回
// 下方 MFC 分发(用户手动打开对话框的行为不变)。无会话在飞时走无锁快速路径。
if (McpServer().OnManagedSubPacket(ContextObject, szBuffer, len)) {
return;
}
// 【L】:主机上下线和授权
// 【x】:对话框相关功能
switch (cmd) {
@@ -7445,8 +7472,10 @@ std::pair<std::string, std::string> CMy2015RemoteDlg::GenerateRenewalInfo(
newHmac = std::to_string(hmacVal);
}
// 新 passcode 的到期日是“今天 + days” (见上面的 newPasscode);
// 预设日期已过去时 days 被钳制为 1,日志应显示实际日期。
Mprintf("[TOKEN_AUTH] 生成续期: %s, %s -> %s, %d并发数, 配额剩余%d\n",
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum, renewal.Quota - 1);
sn.c_str(), currentExpire.c_str(), getDateStr(days).c_str(), renewal.HostNum, renewal.Quota - 1);
// 配额递减,配额用完后自动清除预设续期记录
DecrementPendingQuota(sn);
@@ -7546,13 +7575,18 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
int days = (int)span.GetDays();
if (days <= 0) days = 1;
// 客户端按“今天 + days”签发新授权(CMD_AUTHORIZATION 使用 getDateStr(0/days))。
// 预设日期若已经过去(过期授权在旧版本中设置的预设),实际到期日是今天 + 1,
// 日志和提示必须显示实际日期,而不是已失效的预设日期。
std::string actualExpire = getDateStr(days);
// 构建并发送 CMD_AUTHORIZATION
if (source) {
Mprintf("[%s] 自动下发续期: %s, %s -> %s, %d并发数\n",
source, sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum);
source, sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
} else {
Mprintf("自动下发续期: %s, %s -> %s, %d并发数\n",
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum);
sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
}
BYTE bToken[32] = { CMD_AUTHORIZATION };
@@ -7571,10 +7605,10 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
bool hasRemainingQuota = DecrementPendingQuota(sn);
Mprintf("[HEARTBEAT] 续期下发完成: %s, 剩余配额: %s\n", sn.c_str(), hasRemainingQuota ? "有" : "无");
// 提示续期已下发
std::string expireFmt = renewal.ExpireDate.substr(0, 4) + "-" +
renewal.ExpireDate.substr(4, 2) + "-" +
renewal.ExpireDate.substr(6, 2);
// 提示续期已下发(显示实际到期日)
std::string expireFmt = actualExpire.substr(0, 4) + "-" +
actualExpire.substr(4, 2) + "-" +
actualExpire.substr(6, 2);
std::string tip = sn + g_Lang.Get(std::string(" 已自动续期至 ")) + expireFmt;
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
}
+1 -1
View File
@@ -453,7 +453,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu;
CBitmap m_bmOnline[71];
CBitmap m_bmOnline[75];
uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -328,6 +328,8 @@
<ClInclude Include="KeyBoardDlg.h" />
<ClInclude Include="LangManager.h" />
<ClInclude Include="McpServer.h" />
<ClInclude Include="McpInternal.h" />
<ClInclude Include="McpMachine.h" />
<ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="TlsCertificateDlg.h" />
<ClInclude Include="NetworkDlg.h" />
@@ -397,6 +399,8 @@
<ClCompile Include="WebService.cpp" />
<ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpInternal.cpp" />
<ClCompile Include="McpMachine.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="TlsCertificateDlg.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c">
@@ -580,6 +584,8 @@
<Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\TlsDisable.bmp" />
<Image Include="res\Bitmap\TlsEnable.bmp" />
<Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" />
@@ -89,6 +89,8 @@
<ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpInternal.cpp" />
<ClCompile Include="McpMachine.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="TlsCertificateDlg.cpp" />
<ClCompile Include="CClientLog.cpp" />
@@ -203,6 +205,8 @@
<ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="McpServer.h" />
<ClInclude Include="McpInternal.h" />
<ClInclude Include="McpMachine.h" />
<ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="TlsCertificateDlg.h" />
<ClInclude Include="CClientLog.h" />
@@ -359,5 +363,7 @@
<Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" />
<Image Include="res\Bitmap\TlsEnable.bmp" />
<Image Include="res\Bitmap\TlsDisable.bmp" />
</ItemGroup>
</Project>
+21 -11
View File
@@ -258,6 +258,7 @@ void CBuildDlg::DoDataExchange(CDataExchange* pDX)
DDX_Control(pDX, IDC_EDIT_DOWNLOAD_URL, m_EditDownloadUrl);
DDX_Text(pDX, IDC_EDIT_DOWNLOAD_URL, m_sDownloadUrl);
DDV_MaxChars(pDX, m_sDownloadUrl, 255);
DDX_Control(pDX, IDC_COMBO_INSTALL_LOC, m_ComboLocation);
}
@@ -426,7 +427,7 @@ void CBuildDlg::OnBnClickedOk()
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
m_SliderClientSize.SetPos(0);
}
int startup = Startup_DLL;
int startup = Startup_DLL, installFlag = m_ComboLocation.GetCurSel();
CString file;
CString targetDir;
switch (index) {
@@ -434,7 +435,7 @@ void CBuildDlg::OnBnClickedOk()
case IndexTestRun_MemDLL:
case IndexTestRun_InjSC:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = index == IndexTestRun_DLL ? CLIENT_TYPE_DLL : CLIENT_TYPE_MEMDLL;
startup = std::map<int, int> {
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
@@ -444,14 +445,14 @@ void CBuildDlg::OnBnClickedOk()
break;
case IndexGhost:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexGhostMsc:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE;
startup = Startup_GhostMsc;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
@@ -459,7 +460,7 @@ void CBuildDlg::OnBnClickedOk()
break;
case IndexTestRunMsc:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunMsc;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
@@ -467,14 +468,14 @@ void CBuildDlg::OnBnClickedOk()
break;
case IndexServerDll:
file = "ServerDll.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_DLL;
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
break;
case IndexTinyRun:
file = "TinyRun.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_SHELLCODE;
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
@@ -497,7 +498,7 @@ void CBuildDlg::OnBnClickedOk()
}
case IndexGhostSystem:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE;
startup = Startup_GhostSystem;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
@@ -505,14 +506,14 @@ void CBuildDlg::OnBnClickedOk()
break;
case IndexTestRunSystem:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunSystem;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case OTHER_ITEM: {
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir, installFlag);
m_OtherItem.GetWindowTextA(file);
typ = -1;
if (file != _TR("未选择文件")) {
@@ -547,6 +548,7 @@ void CBuildDlg::OnBnClickedOk()
memcpy(g_ConnectAddress.szGroupName, m_sGroupName, m_sGroupName.GetLength());
memcpy(g_ConnectAddress.installDir, m_sInstallDir, m_sInstallDir.GetLength());
memcpy(g_ConnectAddress.installName, m_sInstallName, m_sInstallName.GetLength());
g_ConnectAddress.installFlag = installFlag;
if (!g_ConnectAddress.IsValid()) {
SAFE_DELETE_ARRAY(szBuffer);
@@ -781,7 +783,8 @@ BOOL CBuildDlg::OnInitDialog()
// 多语言翻译 - Static控件
SetDlgItemText(IDC_STATIC_OTHER_ITEM, _TR("未选择文件"));
SetDlgItemText(IDC_STATIC_PAYLOAD, _TR("载荷类型:"));
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("安装目录:"));
SetDlgItemText(IDC_STATIC_LOCATION, _TR("安装目录:"));
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("子目录:"));
SetDlgItemText(IDC_STATIC_PAYLOAD3, _TR("程序名称:"));
SetDlgItemText(IDC_STATIC_DOWNLOAD, _TR("下载地址(默认本机):"));
SetDlgItemText(IDC_STATIC_BUILD_SERVICE, _TR("服务程序:"));
@@ -852,6 +855,13 @@ BOOL CBuildDlg::OnInitDialog()
m_ComboCompress.InsertStringL(CLIENT_COMP_SC_AES_OLD_UPX, "SC AES<Old> + UPX");
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_SC_AES_OLD);
m_ComboLocation.InsertStringL(0, "ProgramData");
m_ComboLocation.InsertStringL(1, "ProgramFiles - Admin");
m_ComboLocation.InsertStringL(2, "SystemRoot - Admin");
m_ComboLocation.InsertStringL(3, "APPDATA");
m_ComboLocation.InsertStringL(4, "LOCALAPPDATA");
m_ComboLocation.SetCurSel(0);
m_ComboPayload.InsertStringL(Payload_Self, "载荷写入当前程序尾部");
m_ComboPayload.InsertStringL(Payload_Raw, "载荷写入单独的二进制文件");
m_ComboPayload.InsertStringL(Payload_BMP, "载荷写入 BMP 格式图片");
+1
View File
@@ -107,4 +107,5 @@ public:
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
void EnableWindowsOnlyControls(BOOL enable);
CComboBox m_ComboLocation;
};
+21 -5
View File
@@ -563,6 +563,17 @@ void CLicenseDlg::OnLicenseRenewal()
if (defaultHostNum <= 0) defaultHostNum = 100;
int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1;
// 判断当前授权是否已过期。续期从“今天”起算:客户端收到 days 后按
// getDateStr(0) + days 签发新授权(见 CMD_AUTHORIZATION 处理)。
// 若从已过去的原到期日加天数,预设日期会落在过去,下发时被
// SendPendingRenewal 钳制成 1 天,实际到期日与预设/提示不一致。
CTime now = CTime::GetCurrentTime();
CTime today(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
CString strToday;
strToday.Format(_T("%04d%02d%02d"), now.GetYear(), now.GetMonth(), now.GetDay());
bool expired = currentExpireDate.length() == 8 &&
currentExpireDate < std::string(CT2A(strToday));
// 格式化当前过期日期显示
CString strCurrentExpire;
if (currentExpireDate.length() == 8) {
@@ -570,6 +581,9 @@ void CLicenseDlg::OnLicenseRenewal()
currentExpireDate.substr(0, 4).c_str(),
currentExpireDate.substr(4, 2).c_str(),
currentExpireDate.substr(6, 2).c_str());
if (expired) {
strCurrentExpire += _L(" (已过期,续期从今天起算)");
}
} else {
strCurrentExpire = _TR("当前到期: 未知");
}
@@ -595,15 +609,13 @@ void CLicenseDlg::OnLicenseRenewal()
return;
}
// 计算新的过期日期 (从原到期日期开始加天数)
CTime baseTime;
if (currentExpireDate.length() == 8) {
// 计算新的过期日期:未过期从原到期日加天数;已过期从今天起算。
CTime baseTime = today;
if (!expired && currentExpireDate.length() == 8) {
int year = atoi(currentExpireDate.substr(0, 4).c_str());
int month = atoi(currentExpireDate.substr(4, 2).c_str());
int day = atoi(currentExpireDate.substr(6, 2).c_str());
baseTime = CTime(year, month, day, 0, 0, 0);
} else {
baseTime = CTime::GetCurrentTime(); // 无法解析时用当前日期
}
CTimeSpan span(days, 0, 0, 0);
CTime newExpireTime = baseTime + span;
@@ -635,6 +647,10 @@ void CLicenseDlg::OnLicenseRenewal()
CString msg;
msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权",
strPending, hostNum, quota, quota);
if (expired) {
msg += _T("\n");
msg += _L("(原授权已过期,续期从今天起算)");
}
MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION);
}
}
+4
View File
@@ -114,5 +114,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
// 登录时上报的 TLS 传输标志(RES_TLS=="1" → "Y")。老客户端不上报,取值为 "N"。
// 注意这是登录凭据而不是实时探测:连接建立后若配置发生变化,需要客户端重连才会更新。
device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");
return device;
}
+172
View File
@@ -0,0 +1,172 @@
#include "stdafx.h"
#include "McpInternal.h"
#include "context.h" // context 接口(GetClientID/IsLogin)
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs)+ WM_SHOWERRORMSG
#include "LangManager.h" // _TR
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
namespace McpInternal {
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
b["indentation"] = "";
return Json::writeString(b, v);
}
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
resp["result"] = result;
return JsonToString(resp);
}
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
Json::Value err(Json::objectValue);
err["code"] = code;
err["message"] = msg;
resp["error"] = err;
return JsonToString(resp);
}
std::string ToLowerAscii(const std::string& s) {
std::string r = s;
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return r;
}
bool ContainsCI(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
}
std::string JsonStrField(const Json::Value& v, const char* key) {
if (v.isObject() && v.isMember(key) && v[key].isString())
return v[key].asString();
return "";
}
bool IsDigits(const std::string& s) {
if (s.empty()) return false;
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key);
}
bool GetIntArg(const Json::Value& args, const char* key, int& out) {
if (!args.isMember(key)) return false;
const Json::Value& v = args[key];
if (v.isNumeric()) { out = v.asInt(); return true; }
if (v.isString()) {
const std::string s = v.asString();
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
out = atoi(s.c_str());
return true;
}
}
return false;
}
bool ReadUInt64(const Json::Value& v, uint64_t& out) {
if (v.isNumeric()) { out = v.asUInt64(); return true; }
if (v.isString()) {
std::string s = v.asString();
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
}
return false;
}
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
if (!args.isMember(key)) return false;
return ReadUInt64(args[key], out);
}
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
err = "Missing required parameter: id";
return false;
}
if (!IsDigits(sid)) {
err = "Invalid id: expected a decimal host id string";
return false;
}
out = strtoull(sid.c_str(), nullptr, 10);
return true;
}
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
if (!parent) return nullptr;
context* found = nullptr;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
found = ctx;
break;
}
}
LeaveCriticalSection(&parent->m_cs);
return found;
}
size_t BoundedStrlen(const char* p, size_t avail) {
size_t n = 0;
while (n < avail && p[n]) ++n;
return n;
}
std::string ToUtf8(const char* s, UINT cp) {
if (!s || !*s) return "";
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string out(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
return out;
}
std::string ToAnsi(const std::string& utf8, UINT cp) {
if (utf8.empty()) return "";
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
if (wlen <= 0) return utf8;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
if (alen <= 0) return utf8;
std::string out(alen, '\0');
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
return out;
}
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& tool, const std::string& detail) {
if (!parent) return;
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP管理操作")));
}
} // namespace McpInternal
+59
View File
@@ -0,0 +1,59 @@
#pragma once
// MCP 工具实现共享的内部辅助函数。
//
// 这些函数原先都定义在 McpServer.cpp 的匿名命名空间里;主机管理 / 连接管理
// 工具族(McpMachine.cpp)需要复用同一套 JSON、host id 解析与编码转换逻辑,
// 故抽出到独立编译单元,由 McpServer.cpp 与 McpMachine.cpp 共同引用。
//
// 约定:函数语义与实现与抽取前完全一致(McpServer.cpp 通过 using-directive
// 引入,调用点不变),不做行为改动。
#include <Windows.h>
#include "jsoncpp/json.h"
#include <string>
#include <cstdint>
class CMy2015RemoteDlg;
class context;
namespace McpInternal {
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v);
// JSON-RPC 2.0 成功 / 错误响应
std::string BuildResult(const Json::Value& id, const Json::Value& result);
std::string BuildError(const Json::Value& id, int code, const std::string& msg);
// 仅 ASCII 小写化;不区分大小写的子串匹配
std::string ToLowerAscii(const std::string& s);
bool ContainsCI(const std::string& haystack, const std::string& needle);
// 入参读取
std::string JsonStrField(const Json::Value& v, const char* key);
bool IsDigits(const std::string& s);
std::string GetStringArg(const Json::Value& args, const char* key);
bool GetIntArg(const Json::Value& args, const char* key, int& out);
bool ReadUInt64(const Json::Value& v, uint64_t& out);
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out);
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false 并写 err。
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err);
// 在 m_HostList 中按 clientID 找在线主 context(锁内遍历 m_cs)。
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id);
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
size_t BoundedStrlen(const char* p, size_t avail);
// 编码转换:任意代码页 → UTF-8(输出 JSON 用);UTF-8 → 目标代码页(下发客户端用)。
std::string ToUtf8(const char* s, UINT cp);
std::string ToAnsi(const std::string& utf8, UINT cp);
// 写操作审计:PostMessageA(WM_SHOWERRORMSG) 落到服务端消息/审计日志(不可关闭)。
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& tool, const std::string& detail);
} // namespace McpInternal
File diff suppressed because it is too large. Load diff
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// 主机管理(10 个标签页)与连接管理(连接列表 / 强制断开)MCP 工具族。
//
// 协议(详见 docs/Mcp_Machine_Design.md):
// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 MachineManager 子连接回
// TOKEN_SYSINFOLIST(61) → 发 1 字节 tab(0..9) → 回 TOKEN_MACHINE_*(31..42)。
// 连接管理:主连接 COMMAND_CONNECTION(253) → CConnectionsManager 子连接回
// TOKEN_CONNECTION_LIST(255) → 发 254+原始 110 字节记录强制断开。
//
// 会话状态由 CMcpServer 持有(与 m_Pending 共用单飞行表),本模块只负责工具 schema、
// 参数校验、组包与解析。
#include "jsoncpp/json.h"
#include <string>
class CMy2015RemoteDlg;
namespace McpMachine {
// 是否由本模块处理(AppendTools 与 Call 必须保持一致)。
bool Handles(const std::string& toolName);
// 按当前开关(McpMachine / McpMachineDangerous / McpReadonly)追加可见工具。
void AppendTools(Json::Value& tools);
// 执行工具;返回完整 JSON-RPC 响应字符串(成功 = BuildResult,失败 = BuildError)。
std::string Call(const Json::Value& id, const std::string& toolName,
const Json::Value& args, CMy2015RemoteDlg* parent);
} // namespace McpMachine
+30 -176
View File
@@ -8,6 +8,8 @@
#include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
#include "LangManager.h" // _TR(审计日志标题语言映射)
#include "common/file_upload.h" // V2 文件传输(RecvFileChunkV2 / HandleFileCompleteV2 / 包结构)
#include "McpInternal.h" // 共享助手(McpMachine.cpp 也使用)
#include "McpMachine.h" // 主机管理 / 连接管理工具族
#include <sstream>
@@ -30,39 +32,15 @@
std::string GetPwdHash();
std::string GetHMAC(int offset);
// 共享助手(JsonToString/BuildResult/ParseHostIdArg/FindMainContext/ToUtf8/ToAnsi/...)
// 已抽到 McpInternal.cpp;此处引入后调用点保持原样。
using namespace McpInternal;
namespace {
// P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。
static const int kMcpToolTimeoutMs = 20000;
// Json::Value → 紧凑 JSON 字符串
std::string JsonToString(const Json::Value& v) {
Json::StreamWriterBuilder b;
b["indentation"] = "";
return Json::writeString(b, v);
}
// JSON-RPC 2.0 成功响应
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
resp["result"] = result;
return JsonToString(resp);
}
// JSON-RPC 2.0 错误响应
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
Json::Value resp(Json::objectValue);
resp["jsonrpc"] = "2.0";
resp["id"] = id;
Json::Value err(Json::objectValue);
err["code"] = code;
err["message"] = msg;
resp["error"] = err;
return JsonToString(resp);
}
// initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo)
std::string BuildInitializeResult(const Json::Value& id) {
Json::Value result(Json::objectValue);
@@ -84,33 +62,6 @@ std::string BuildPingResult(const Json::Value& id) {
// ========== P2a 通用辅助 ==========
// 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配
std::string ToLowerAscii(const std::string& s) {
std::string r = s;
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return r;
}
// 不区分大小写的子串匹配
bool ContainsCI(const std::string& haystack, const std::string& needle) {
if (needle.empty()) return true;
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
}
// 取对象的字符串字段,缺失/非字符串返回 ""
std::string JsonStrField(const Json::Value& v, const char* key) {
if (v.isObject() && v.isMember(key) && v[key].isString())
return v[key].asString();
return "";
}
// 是否纯数字(host id 为 uint64 十进制字符串)
bool IsDigits(const std::string& s) {
if (s.empty()) return false;
for (char c : s) if (c < '0' || c > '9') return false;
return true;
}
// 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象)
Json::Value GetCallArguments(const Json::Value& params) {
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
@@ -118,85 +69,8 @@ Json::Value GetCallArguments(const Json::Value& params) {
return Json::Value(Json::objectValue);
}
// 读取可选字符串入参,缺失返回 ""
std::string GetStringArg(const Json::Value& args, const char* key) {
return JsonStrField(args, key);
}
// 读取可选整数入参(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command /
// terminal_exec 等工具复用。
static bool GetIntArg(const Json::Value& args, const char* key, int& out);
// ========== P2b 辅助 ==========
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
std::string sid = GetStringArg(args, "id");
if (sid.empty()) {
err = "Missing required parameter: id";
return false;
}
if (!IsDigits(sid)) {
err = "Invalid id: expected a decimal host id string";
return false;
}
out = strtoull(sid.c_str(), nullptr, 10);
return true;
}
// 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
if (!parent) return nullptr;
context* found = nullptr;
EnterCriticalSection(&parent->m_cs);
for (context* ctx : parent->m_HostList) {
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
found = ctx;
break;
}
}
LeaveCriticalSection(&parent->m_cs);
return found;
}
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
size_t BoundedStrlen(const char* p, size_t avail) {
size_t n = 0;
while (n < avail && p[n]) ++n;
return n;
}
// 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。
std::string ToUtf8(const char* s, UINT cp) {
if (!s || !*s) return "";
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string out(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
return out;
}
// 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的
// CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。
// 非 UTF-8(如纯 ASCII)失败时原样返回。
std::string ToAnsi(const std::string& utf8, UINT cp) {
if (utf8.empty()) return "";
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
if (wlen <= 0) return utf8;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
if (alen <= 0) return utf8;
std::string out(alen, '\0');
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
return out;
}
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
@@ -647,6 +521,10 @@ Json::Value BuildHostItemSchema() {
Json::Value onlineProp(Json::objectValue);
onlineProp["type"] = "boolean";
itemProps["online"] = onlineProp;
// 客户端登录时上报的 TLS 传输标志(老客户端恒为 false)
Json::Value tlsProp(Json::objectValue);
tlsProp["type"] = "boolean";
itemProps["tls"] = tlsProp;
return itemProps;
}
@@ -1311,7 +1189,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
tool["name"] = "list_online_hosts";
// 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK,
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,非实时探测;老客户端恒为 false)。";
Json::Value inputSchema(Json::objectValue);
inputSchema["type"] = "object";
@@ -1328,7 +1206,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "search_hosts";
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。";
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
tool["inputSchema"] = BuildSearchHostsInputSchema();
tool["outputSchema"] = BuildHostOutputSchema();
@@ -1340,7 +1218,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "get_host_detail";
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。";
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
tool["inputSchema"] = BuildGetHostDetailInputSchema();
tool["outputSchema"] = BuildHostDetailOutputSchema();
@@ -1607,6 +1485,11 @@ std::string BuildToolsListResult(const Json::Value& id) {
}
}
// 19) 连接管理 / 主机管理(P8:list_network_connections / kill_network_connection /
// host_* 共 16 个工具)。开关在 McpMachine.cpp 内部按 McpMachine /
// McpMachineDangerous / McpReadonly 逐项判断,关闭时不追加任何工具。
McpMachine::AppendTools(tools);
result["tools"] = tools;
return BuildResult(id, result);
}
@@ -3366,21 +3249,6 @@ static bool GetNormCoord(const Json::Value& args, const char* key, double& out,
return true;
}
// 读取可选整数参数(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
static bool GetIntArg(const Json::Value& args, const char* key, int& out) {
if (!args.isMember(key)) return false;
const Json::Value& v = args[key];
if (v.isNumeric()) { out = v.asInt(); return true; }
if (v.isString()) {
const std::string s = v.asString();
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
out = atoi(s.c_str());
return true;
}
}
return false;
}
// button 字符串 → 0=左/1=中/2=右;空或 left 默认左。
static bool ParseButton(const std::string& s, int& out) {
if (s.empty() || s == "left") { out = 0; return true; }
@@ -4351,31 +4219,6 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
// ========== P7:设备管理控制 ==========
// 读取 JSON number 或纯数字字符串为 uint64。
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
if (v.isNumeric()) { out = v.asUInt64(); return true; }
if (v.isString()) {
std::string s = v.asString();
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
}
return false;
}
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
if (!args.isMember(key)) return false;
return ReadUInt64(args[key], out);
}
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& tool, const std::string& detail) {
if (!parent) return;
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP管理操作")));
}
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
@@ -4687,6 +4530,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value args = GetCallArguments(params);
// P8:连接管理 / 主机管理工具族(独立分发;门控在 McpMachine::Call 内复核)。
if (McpMachine::Handles(toolName)) return McpMachine::Call(id, toolName, args, parent);
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
@@ -5225,13 +5071,18 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res)
bool CMcpServer::IsPending(uint64_t device_id) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
return m_Pending.find(device_id) != m_Pending.end();
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
// P8:主机管理/连接管理会话的响应包走 OnManagedSubPacket,不归 MFC 的
// TOKEN_PSLIST/TOKEN_WSLIST/SERVERLIST/DRIVE_LIST 分支消费(否则会抢走 UI 包)。
return it->second.kind == PendingKind::Generic || it->second.kind == PendingKind::FollowUp;
}
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
if (it->second.kind != PendingKind::Generic && it->second.kind != PendingKind::FollowUp) return; // P8 会话:不接管
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
it->second.data.assign(data, data + len);
it->second.done = true;
@@ -5416,6 +5267,7 @@ bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& too
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.kind = PendingKind::FollowUp;
r.followUp = followUp;
m_Pending[device_id] = std::move(r);
return true;
@@ -5501,6 +5353,8 @@ void CMcpServer::OnManageSubClosed(context* subCtx) {
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
for (auto& kv : m_Pending) {
if (kv.second.subCtx == subCtx) {
// P8 会话由 OnManagedContextClosed 处理(这里若清 subCtx 会让其无法按指针反查设备)。
if (kv.second.kind == PendingKind::Machine || kv.second.kind == PendingKind::ConnMgr) continue;
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
(void*)subCtx, kv.second.tool.c_str());
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
+78
View File
@@ -270,6 +270,41 @@ public:
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
void OnManageSubClosed(context* subCtx);
// ===== P8:主机管理 / 连接管理(子连接会话)=====
// 协议:主连接下发 COMMAND_MACHINE_MANAGE(162) / COMMAND_CONNECTION(253) → 客户端新建
// 子连接 → 子连接回就绪包(TOKEN_SYSINFOLIST=61 / TOKEN_CONNECTION_LIST=255)→ 在同一
// 子连接上多步收发。会话复用 m_Pending 表(统一「单设备单飞行」),kind 区分工具族。
// 见 docs/Mcp_Machine_Design.md。
void SetMachineEnabled(bool enabled) { m_machineEnabled = enabled; }
bool IsMachineEnabled() const { return m_machineEnabled; }
void SetMachineDangerousEnabled(bool enabled) { m_machineDangerousEnabled = enabled; }
bool IsMachineDangerousEnabled() const { return m_machineDangerousEnabled; }
// MessageHandle 顶部钩子(终端拦截之后、switch 之前):true = 本包已由 MCP 消费。
// 已绑定的子连接:只保存期望 token 的包与 TOKEN_MACHINE_MSG(42),其余丢弃。
// 未绑定的子连接:仅 61/255 可作为就绪包尝试绑定;否则返回 false(MFC 行为不变)。
bool OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len);
// OfflineProc 钩子:接管的子连接断开 → 置 closed 唤醒等待者(幂等)。
void OnManagedContextClosed(context* subCtx);
// 工具线程:登记会话(false = 该设备已有挂起请求在飞 → 设备忙)。
bool BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx);
bool BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx);
// 工具线程:发命令前清空上一次响应并登记期望 token。
void ArmManagedResponse(uint64_t device_id, BYTE expectedToken);
// 工具线程:等待子连接就绪包 / 指定 token 的响应(超时由调用方按步给)。
enum class ManagedWaitResult { Ok, Timeout, Closed, ClientError };
ManagedWaitResult WaitManagedPacket(uint64_t device_id, BYTE token,
std::vector<BYTE>& out, context*& subCtxOut,
int timeoutMs, std::string& clientError);
// 工具线程:结束会话;cancelIo = 关闭子连接;markOrphan = 打 5s 墓碑
// (吸收迟到的 61/255,避免超时后弹出 MFC 对话框)。
void EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
@@ -285,6 +320,15 @@ private:
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
void HandleMcp(const httplib::Request& req, httplib::Response& res);
// 挂起请求类型:主机管理/连接管理会话与既有一次性请求共用 m_Pending 保证单设备单飞行,
// 但 MessageHandle 的 P2b/P3/P7 拦截分支(IsPending/TakeMainResponse)必须把这两类排除,
// 否则用户手动打开对话框时 UI 包会被 MCP 抢走。
enum class PendingKind { Generic, FollowUp, Machine, ConnMgr };
static bool KindAcceptsToken(PendingKind kind, BYTE token);
bool BeginManagedSession(uint64_t device_id, const std::string& tool,
PendingKind kind, context* mainCtx);
httplib::Server m_server;
std::thread m_thread;
std::atomic<bool> m_running{false};
@@ -306,11 +350,43 @@ private:
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
// ===== P8:主机管理 / 连接管理会话(kind = Machine / ConnMgr 时有效)=====
PendingKind kind = PendingKind::Generic;
context* mainCtx = nullptr; // Begin 时抓取的主连接(只作身份校验,不从其收发)
std::string peer; // mainCtx->GetPeerName()(老客户端子连接 ID=0 时兜底)
BYTE expectedToken = 0; // 期望的响应 token(0 = 未设置)
bool hasResponse = false;
std::vector<BYTE> response; // 含首字节 token
std::string clientError; // TOKEN_MACHINE_MSG(42) 文本(已转 UTF-8)
bool closed = false; // 子连接在等待期间断开
std::chrono::steady_clock::time_point deadline; // 整个会话的截止时间
};
std::mutex m_PendingMutex;
std::condition_variable m_PendingCv;
std::map<uint64_t, PendingRequest> m_Pending;
// ===== P8:子连接路由(受 m_PendingMutex 保护)=====
// 已绑定子连接 → device(该子连接的一切包都由 MCP 消费)。
std::map<context*, uint64_t> m_SubCtxToDevice;
// CancelIO 之后仍可能到达的包:静默丢弃(避免子连接回收前的尾巴包弹对话框)。
std::set<context*> m_AbsorbedSubCtx;
// 会话异常结束后 5s 内的迟到就绪包墓碑(device / peer 两个维度)。
struct OrphanMark {
std::chrono::steady_clock::time_point expiry;
PendingKind kind = PendingKind::Generic;
};
std::map<uint64_t, OrphanMark> m_OrphanByDevice;
std::map<std::string, OrphanMark> m_OrphanByPeer;
// 清理过期墓碑(持 m_PendingMutex 调用)。
void PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now);
// 路由表大小(已绑定 + 已吸收的子连接数)。MessageHandle 每个包都会调用
// OnManagedSubPacket,若无会话在飞则用它无锁快速返回,避免音频/屏幕等高频
// 子连接逐包争用 m_PendingMutex。只在持锁时更新(store)。
std::atomic<int> m_ManagedRoutes{0};
void RefreshManagedRoutesLocked();
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
std::atomic<uint16_t> m_PreviewReqId{1};
@@ -381,6 +457,8 @@ private:
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
bool m_machineEnabled = false; // P8 主机管理族总开关(默认关;16 个 host_* 工具)
bool m_machineDangerousEnabled = false; // P8 高风险子集(冻结/解冻进程、停止/重启服务)
};
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
+35 -4
View File
@@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal()
{
USES_CONVERSION;
CString title = _TR("MCP设置");
BuildDialogTemplate(m_Template, T2CW(title), 320, 420);
BuildDialogTemplate(m_Template, T2CW(title), 320, 480);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
@@ -148,6 +148,12 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MANAGE);
m_btnMachine.Create(_TR("启用主机管理(进程/窗口/服务/任务/网络状态)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MACHINE);
m_btnMachineDangerous.Create(_TR("启用危险主机操作(冻结/解冻进程、停止/重启服务)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MACHINE_DANGEROUS);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
@@ -172,6 +178,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnMachine.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnMachineDangerous.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
@@ -189,6 +197,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
int machine = THIS_CFG.GetInt("settings", "McpMachine", 0);
int machineDangerous = THIS_CFG.GetInt("settings", "McpMachineDangerous", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
@@ -200,6 +210,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
m_btnMachine.SetCheck(machine ? BST_CHECKED : BST_UNCHECKED);
m_btnMachineDangerous.SetCheck(machineDangerous ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
@@ -220,6 +232,16 @@ void CMcpSettingsDlg::OnOK()
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
bool machine = (m_btnMachine.GetCheck() == BST_CHECKED);
bool machineDangerous = (m_btnMachineDangerous.GetCheck() == BST_CHECKED);
// 危险子集依附于主机管理总开关:没开主机管理就没有工具可放开,直接清除并提示。
if (machineDangerous && !machine) {
machineDangerous = false;
m_btnMachineDangerous.SetCheck(BST_UNCHECKED);
MessageBoxL(_TR("危险主机操作依附于主机管理:请先启用主机管理。"), _TR("提示"),
MB_ICONWARNING);
}
// 端口校验:1-65535
int port = atoi(CT2A(sPort));
@@ -247,14 +269,17 @@ void CMcpSettingsDlg::OnOK()
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
THIS_CFG.SetInt("settings", "McpMachine", machine ? 1 : 0);
THIS_CFG.SetInt("settings", "McpMachineDangerous", machineDangerous ? 1 : 0);
std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
// 拆成多段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") +
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。"),
_TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK();
@@ -300,6 +325,12 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnMachine.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnMachineDangerous.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
+4
View File
@@ -32,6 +32,8 @@ private:
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
IDC_MCP_MACHINE = 1011, // 「启用主机管理」复选框(host_* 工具族)
IDC_MCP_MACHINE_DANGEROUS = 1012, // 「启用危险主机操作」复选框(冻结/停止/重启)
};
CButton m_btnEnable;
@@ -42,6 +44,8 @@ private:
CButton m_btnRemoteControl;
CButton m_btnFileTransfer;
CButton m_btnManage;
CButton m_btnMachine;
CButton m_btnMachineDangerous;
CStatic m_lblWhitelist;
CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel;
+76
View File
@@ -349,6 +349,75 @@ public:
void onHttp(HttpHandler handler) { m_httpHandler = handler; }
private:
// ---- Real client IP behind a reverse proxy ----
// A same-host nginx/Caddy connects from 127.0.0.1 and reports the original
// peer in X-Real-IP / X-Forwarded-For. Only trust those headers when the
// TCP peer is loopback: otherwise any internet client could forge its
// address in the audit log and rotate it to evade login throttling.
static std::string trimHttpValue(const std::string& s) {
size_t b = s.find_first_not_of(" \t");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
static std::string asciiLower(const std::string& s) {
std::string out = s;
for (char& c : out) {
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
}
return out;
}
// Case-insensitive lookup of one header in the raw request. The request
// line is first, so matching "\r\n<name>:" cannot hit the request target.
static std::string httpHeader(const std::string& request, const char* name) {
const std::string needle = "\r\n" + asciiLower(name) + ":";
const std::string hay = asciiLower(request);
size_t pos = hay.find(needle);
if (pos == std::string::npos) return "";
size_t vstart = pos + needle.size();
size_t vend = hay.find("\r\n", vstart);
return trimHttpValue(request.substr(vstart,
vend == std::string::npos ? std::string::npos : vend - vstart));
}
// Accept only a bare IPv4/IPv6 literal; forwarded headers are attacker
// controlled even when the proxy is trusted, so no hostnames/ports.
static std::string normalizeIPLiteral(const std::string& value) {
std::string s = trimHttpValue(value);
if (s.empty() || s.size() > 63) return "";
struct in_addr v4;
if (inet_pton(AF_INET, s.c_str(), &v4) == 1) return s;
struct in6_addr v6;
if (inet_pton(AF_INET6, s.c_str(), &v6) == 1) return s;
return "";
}
static std::string forwardedClientIP(const std::string& request) {
// X-Forwarded-For is "client, proxy1, ..." with the oldest hop first.
// Our own proxy appends the peer it saw, so the last valid entry is
// the one it actually observed: a client-supplied list cannot forge
// it. Prefer this over X-Real-IP, which is only trustworthy when the
// proxy explicitly overwrites it (some configs pass it through).
std::string xff = httpHeader(request, "x-forwarded-for");
size_t end = xff.size();
while (end > 0) {
size_t comma = xff.rfind(',', end - 1);
size_t start = comma == std::string::npos ? 0 : comma + 1;
std::string ip = normalizeIPLiteral(xff.substr(start, end - start));
if (!ip.empty()) return ip;
if (comma == std::string::npos) break;
end = comma;
}
return normalizeIPLiteral(httpHeader(request, "x-real-ip"));
}
static bool isLoopbackPeer(const std::string& ip) {
return ip.rfind("127.", 0) == 0 || ip == "::1" ||
ip == "::ffff:127.0.0.1";
}
// Parse Proxy Protocol v2 header and extract real client IP
bool parseProxyProtocolV2(SOCKET sock, std::string& clientIP) {
// PP2 signature (12 bytes)
@@ -448,6 +517,13 @@ private:
if (request.find("\r\n\r\n") != std::string::npos) break;
}
// Behind a same-host reverse proxy the TCP peer is 127.0.0.1; recover
// the real client address the proxy recorded in the handshake.
if (isLoopbackPeer(clientIP)) {
std::string forwarded = forwardedClientIP(request);
if (!forwarded.empty()) clientIP = forwarded;
}
// Check if WebSocket upgrade
if (request.find("Upgrade: websocket") == std::string::npos) {
// Regular HTTP request - extract path
+1 -1
View File
@@ -46,7 +46,7 @@
// 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏
#define BRAND_VERSION "1.4.0"
#define BRAND_VERSION "1.4.1"
// 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
+8 -1
View File
@@ -1992,7 +1992,7 @@ MCP
端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein
Token 不能为空=Token darf nicht leer sein
MCP 设置已保存。=MCP-Einstellungen gespeichert.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen/Maschinenverwaltung/gefaehrlichen Maschinenoperationen werden nach Neustart wirksam.
只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert)
命令白名单=Befehls-Whitelist
MCP命令执行=MCP-Befehlsausfuehrung
@@ -2006,6 +2006,10 @@ Token=Token
启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop)
启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file)
启用管理操作(进程/窗口/服务/电源)=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie)
启用主机管理(进程/窗口/服务/任务/网络状态)=Maschinenverwaltung aktivieren (Prozess/Fenster/Dienst/Aufgabe/Netzwerkstatus)
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Gefaehrliche Maschinenoperationen aktivieren (Prozess einfrieren/auftauen, Dienst stoppen/neu starten)
危险主机操作依附于主机管理:请先启用主机管理。=Gefaehrliche Maschinenoperationen setzen die Maschinenverwaltung voraus. Aktivieren Sie zuerst die Maschinenverwaltung.
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Schreiboperationen der Maschinenverwaltung erfordern deaktivierten Nur-Lesen-Modus; Einfrieren/Auftauen von Prozessen und Stoppen/Neustarten von Diensten erfordern zusaetzlich gefaehrliche Maschinenoperationen.
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
%d 新连接\r\n=%d Neue Verbindung\r\n
%d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n
@@ -2097,3 +2101,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2-Uebertragung gestartet, %d Datei(en) -> %s
V2下载请求已发送,共 %d 个项目=V2-Download-Anfrage gesendet, %d Element(e)
找不到主连接: clientID %llu=Hauptverbindung nicht gefunden: clientID=%llu
(已过期,续期从今天起算)= (abgelaufen; Verlaengerung ab heute)
(原授权已过期,续期从今天起算)=(Urspruengliche Lizenz abgelaufen; Verlaengerung ab heute)
子目录=SubDir
+8 -1
View File
@@ -1997,7 +1997,7 @@ MCP
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
Token 不能为空=Token cannot be empty
MCP 设置已保存。=MCP settings saved.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations/host management/dangerous host operations take effect after restart.
只读模式(禁命令执行)=Read-only mode (disable command execution)
命令白名单=Command whitelist
MCP命令执行=MCP command execution
@@ -2011,6 +2011,10 @@ Token=Token
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
启用文件传输(download_file)=Enable file transfer (download_file)
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
启用主机管理(进程/窗口/服务/任务/网络状态)=Enable host management (processes/windows/services/tasks/network state)
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Enable dangerous host operations (freeze/thaw process, stop/restart service)
危险主机操作依附于主机管理:请先启用主机管理。=Dangerous host operations depend on host management. Enable host management first.
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Host management write operations require read-only mode to be off; freezing/thawing processes and stopping/restarting services also require dangerous host operations.
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
%d 新连接\r\n=%d New connection\r\n
%d 本地连接断开\r\n=%d Local connection closed\r\n
@@ -2112,3 +2116,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2 transfer started, %d file(s) -> %s
V2下载请求已发送,共 %d 个项目=V2 download request sent, %d item(s)
找不到主连接: clientID %llu=Main connection not found: clientID=%llu
(已过期,续期从今天起算)= (expired; renewal starts today)
(原授权已过期,续期从今天起算)=(Original license expired; renewal starts today)
子目录=SubDir
+8 -1
View File
@@ -1988,7 +1988,7 @@ MCP
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
Token 不能为空=Token 不能為空
MCP 设置已保存。=MCP 設定已儲存。
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作/主機管理/危險主機操作的變更需重新啟動程式後生效。
只读模式(禁命令执行)=唯讀模式(禁命令執行)
命令白名单=命令白名單
MCP命令执行=MCP命令執行
@@ -2002,6 +2002,10 @@ Token=Token
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
启用文件传输(download_file)=啟用檔案傳輸(download_file)
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
启用主机管理(进程/窗口/服务/任务/网络状态)=啟用主機管理(處理程序/視窗/服務/排程工作/網路狀態)
启用危险主机操作(冻结/解冻进程、停止/重启服务)=啟用危險主機操作(凍結/解凍處理程序、停止/重新啟動服務)
危险主机操作依附于主机管理:请先启用主机管理。=危險主機操作依附於主機管理:請先啟用主機管理。
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=主機管理的寫入操作需關閉唯讀模式;凍結/解凍處理程序、停止/重新啟動服務還需啟用危險主機操作。
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
%d 新连接\r\n=%d 新連線\r\n
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
@@ -2103,3 +2107,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2 傳輸已啟動,共 %d 個檔案 -> %s
V2下载请求已发送,共 %d 个项目=V2 下載請求已傳送,共 %d 個項目
找不到主连接: clientID %llu=找不到主連線: clientID=%llu
(已过期,续期从今天起算)= (已過期,續期從今天起算)
(原授权已过期,续期从今天起算)=(原授權已過期,續期從今天起算)
子目录=子目录
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+76 -79
View File
@@ -48,9 +48,6 @@
#define IDI_ICON_STRING 163
#define IDD_DIALOG_SERVICES 164
#define IDR_MENU_SERVICES 165
#define IDD_DIALOG_CONNECTIONS 404
#define IDR_MENU_CONNECTIONS 405
#define IDI_CONNECTION 406
#define IDD_DIALOG_VIDEO 166
#define IDC_CURSOR2 168
#define IDC_CURSOR1 171
@@ -266,7 +263,6 @@
#define IDR_WEB_XTERM_CSS 383
#define IDR_WEB_XTERM_FIT_JS 384
#define IDR_WEB_INDEX_HTML 385
#define IDR_WEB_OPUS_DECODER_JS 401
#define IDB_BITMAP_COMPRESS 386
#define IDB_BITMAP_UNCOMPRESS 387
#define IDB_BITMAP9 388
@@ -284,11 +280,21 @@
#define IDB_BITMAP_HISTORY_ACTIVITY 398
#define IDB_BITMAP_VIEW_WND 399
#define IDB_BITMAP_PROXY_AUTORUN 400
#define IDR_WEB_OPUS_DECODER_JS 401
#define IDD_DIALOG_SHARE_HOST 402
#define IDD_FILE_SPEED 403
#define IDD_DIALOG_CONNECTIONS 404
#define IDR_MENU_CONNECTIONS 405
#define IDI_CONNECTION 406
#define IDB_BITMAP_FILE_MANAGER 407
#define IDB_BITMAP_PRIVACY_SHIELD 408
#define IDB_BITMAP_DRAWING_BOARD 409
#define IDB_BITMAP_TLS_ENABLE 410
#define IDB_BITMAP_TLS_DISABLE 411
#define IDB_BITMAP_DESKTOP_QUALITY 412
#define IDB_BITMAP_PREVIEW_QUALITY 413
#define IDB_BITMAP_FILE_SPEED 414
#define IDB_BITMAP_DISCLAIMER 415
#define IDC_MESSAGE 1000
#define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002
@@ -362,8 +368,6 @@
#define IDI_SYSTEM 2035
#define IDC_DOT 2036
#define IDD_AUDIO 2038
#define IDD_QUALITY_LEVELS 3000
#define IDD_PREVIEW_LEVELS 3001
#define IDI_AUDIO 2039
#define IDC_LIST_REMOTE 2040
#define IDC_LIST_LOCAL 2041
@@ -527,6 +531,7 @@
#define IDC_PROGRESS_FILESEND 2214
#define IDC_STATIC_PAYLOAD3 2214
#define IDC_STATIC_CURRENTINDEX 2215
#define IDC_STATIC_LOCATION 2215
#define IDC_STATIC_CURRENTPERCENT 2216
#define IDC_EDIT_INSTALL_DIR 2216
#define IDC_EDIT_INSTALL_NAME 2217
@@ -777,6 +782,52 @@
#define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545
#define IDC_EDIT_LOG 2546
#define IDC_QLEVEL_FPS 2547
#define IDC_QLEVEL_WIDTH 2553
#define IDC_QLEVEL_ALGO 2559
#define IDC_QLEVEL_BITRATE 2565
#define IDC_QLEVEL_RESET 2571
#define IDC_PLEVEL_WIDTH 2572
#define IDC_PLEVEL_QUALITY 2578
#define IDC_PLEVEL_RESET 2584
#define IDC_STATIC_SHARE_ADDRESS 2585
#define IDC_EDIT_SHARE_ADDRESS 2586
#define IDC_CHECK_USE_SHINE 2587
#define IDC_STATIC_SHARE_DAYS 2588
#define IDC_EDIT_SHARE_DAYS 2589
#define IDC_FSPEED_SLIDER 2590
#define IDC_FSPEED_LABEL 2591
#define IDC_FSPEED_RESET 2592
#define IDC_QLEVEL_LABEL_LEVEL 2593
#define IDC_QLEVEL_LABEL_FPS 2594
#define IDC_QLEVEL_LABEL_WIDTH 2595
#define IDC_QLEVEL_LABEL_ALGO 2596
#define IDC_QLEVEL_LABEL_BITRATE 2597
#define IDC_QLEVEL_LABEL_LEVEL0 2598
#define IDC_QLEVEL_LABEL_LEVEL1 2599
#define IDC_QLEVEL_LABEL_LEVEL2 2600
#define IDC_QLEVEL_LABEL_LEVEL3 2601
#define IDC_QLEVEL_LABEL_LEVEL4 2602
#define IDC_QLEVEL_LABEL_LEVEL5 2603
#define IDC_QLEVEL_LABEL_HINT 2604
#define IDC_PLEVEL_LABEL_LEVEL 2605
#define IDC_PLEVEL_LABEL_WIDTH 2606
#define IDC_PLEVEL_LABEL_QUALITY 2607
#define IDC_PLEVEL_LABEL_LEVEL0 2608
#define IDC_PLEVEL_LABEL_LEVEL1 2609
#define IDC_PLEVEL_LABEL_LEVEL2 2610
#define IDC_PLEVEL_LABEL_LEVEL3 2611
#define IDC_PLEVEL_LABEL_LEVEL4 2612
#define IDC_PLEVEL_LABEL_LEVEL5 2613
#define IDC_PLEVEL_LABEL_HINT 2614
#define IDC_FSPEED_HINT 2615
#define IDC_FSPEED_LABEL_SLOWEST 2616
#define IDC_FSPEED_LABEL_DEFAULT 2617
#define IDC_FSPEED_LABEL_UNLIMITED 2618
#define IDC_STATIC_TRIGGER_LIST 2619
#define IDC_COMBO_INSTALL_LOC 2620
#define IDD_QUALITY_LEVELS 3000
#define IDD_PREVIEW_LEVELS 3001
#define ID_ONLINE_UPDATE 32772
#define ID_ONLINE_MESSAGE 32773
#define ID_ONLINE_DELETE 32775
@@ -1006,24 +1057,8 @@
#define ID_MSGLOG_CLEAR 33041
#define ID_CANCEL_SHARE 33042
#define ID_MSGLOG_COPY 33043
#define ID_MSGLOG_SEARCH 33078
#define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081
#define ID_MSGLOG_WHITELIST_IP 33096
#define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_MCP_SETTINGS 33077
#define ID_TLS_CERTIFICATE 33093
#define ID_CONNECTION_MANAGE 33086
#define ID_CONNECTION_KILL 33087
#define ID_CONNECTION_REFRESH 33088
#define ID_TALK_MANAGE 33089
#define ID_EXPERIMENT_FILE_MANAGER 33090
#define ID_EXPERIMENT_PRIVATE_SCREEN 33091
#define ID_EXPERIMENT_DRAWING_BOARD 33092
#define ID_EXPERIMENT_TLS_ENABLE 33094
#define ID_EXPERIMENT_TLS_DISABLE 33095
#define ID_33046 33046
#define ID_PROXY_PORT_AUTORUN 33047
#define ID_TRIGGER_SETTINGS 33048
@@ -1056,73 +1091,35 @@
#define ID_ONLINE_CLIENT_LOG 33074
#define ID_ONLINE_33075 33075
#define ID_ONLINE_FORBIDDEN 33076
#define ID_MCP_SETTINGS 33077
#define ID_MSGLOG_SEARCH 33078
#define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081
#define ID_ONLINE_HISTORY_ACTIVITY 33082
#define ID_EXIT_FULLSCREEN 40001
#define ID_TOOL_QUALITY_LEVELS 33083
#define ID_TOOL_PREVIEW_LEVELS 33084
#define IDC_QLEVEL_FPS 2547
#define IDC_QLEVEL_WIDTH 2553
#define IDC_QLEVEL_ALGO 2559
#define IDC_QLEVEL_BITRATE 2565
#define IDC_QLEVEL_RESET 2571
#define IDC_PLEVEL_WIDTH 2572
#define IDC_PLEVEL_QUALITY 2578
#define IDC_PLEVEL_RESET 2584
#define IDD_DIALOG_SHARE_HOST 402
#define IDC_STATIC_SHARE_ADDRESS 2585
#define IDC_EDIT_SHARE_ADDRESS 2586
#define IDC_CHECK_USE_SHINE 2587
#define IDC_STATIC_SHARE_DAYS 2588
#define IDC_EDIT_SHARE_DAYS 2589
// 文件传输速度调节对话框
#define IDD_FILE_SPEED 403
#define ID_TOOL_FILE_SPEED 33085
#define IDC_FSPEED_SLIDER 2590
#define IDC_FSPEED_LABEL 2591
#define IDC_FSPEED_RESET 2592
// 高级菜单三个对话框的静态文本(显式在 OnInitDialog 设置中文,修复非中文系统乱码)
#define IDC_QLEVEL_LABEL_LEVEL 2593
#define IDC_QLEVEL_LABEL_FPS 2594
#define IDC_QLEVEL_LABEL_WIDTH 2595
#define IDC_QLEVEL_LABEL_ALGO 2596
#define IDC_QLEVEL_LABEL_BITRATE 2597
#define IDC_QLEVEL_LABEL_LEVEL0 2598 // 2598~2603:Ultra..Minimal 六档
#define IDC_QLEVEL_LABEL_LEVEL1 2599
#define IDC_QLEVEL_LABEL_LEVEL2 2600
#define IDC_QLEVEL_LABEL_LEVEL3 2601
#define IDC_QLEVEL_LABEL_LEVEL4 2602
#define IDC_QLEVEL_LABEL_LEVEL5 2603
#define IDC_QLEVEL_LABEL_HINT 2604
#define IDC_PLEVEL_LABEL_LEVEL 2605
#define IDC_PLEVEL_LABEL_WIDTH 2606
#define IDC_PLEVEL_LABEL_QUALITY 2607
#define IDC_PLEVEL_LABEL_LEVEL0 2608 // 2608~2613:Ultra..Minimal 六档
#define IDC_PLEVEL_LABEL_LEVEL1 2609
#define IDC_PLEVEL_LABEL_LEVEL2 2610
#define IDC_PLEVEL_LABEL_LEVEL3 2611
#define IDC_PLEVEL_LABEL_LEVEL4 2612
#define IDC_PLEVEL_LABEL_LEVEL5 2613
#define IDC_PLEVEL_LABEL_HINT 2614
#define IDC_FSPEED_HINT 2615
#define IDC_FSPEED_LABEL_SLOWEST 2616
#define IDC_FSPEED_LABEL_DEFAULT 2617
#define IDC_FSPEED_LABEL_UNLIMITED 2618
// 触发器设置对话框的分组框(显式在 OnInitDialog 设置中文,修复非中文系统乱码)
#define IDC_STATIC_TRIGGER_LIST 2619
#define ID_CONNECTION_MANAGE 33086
#define ID_CONNECTION_KILL 33087
#define ID_CONNECTION_REFRESH 33088
#define ID_TALK_MANAGE 33089
#define ID_EXPERIMENT_FILE_MANAGER 33090
#define ID_EXPERIMENT_PRIVATE_SCREEN 33091
#define ID_EXPERIMENT_DRAWING_BOARD 33092
#define ID_TLS_CERTIFICATE 33093
#define ID_EXPERIMENT_TLS_ENABLE 33094
#define ID_EXPERIMENT_TLS_DISABLE 33095
#define ID_MSGLOG_WHITELIST_IP 33096
#define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 412
#define _APS_NEXT_RESOURCE_VALUE 416
#define _APS_NEXT_COMMAND_VALUE 33097
#define _APS_NEXT_CONTROL_VALUE 2620
#define _APS_NEXT_CONTROL_VALUE 2621
#define _APS_NEXT_SYMED_VALUE 105
#endif
#endif
+48 -30
View File
@@ -721,8 +721,19 @@ void CMachineDlg::ShowTaskList()
continue;
m_list.InsertItem(i, str);
// 目录列:只去掉末尾“\任务名”。不能用 Replace 全量替换——任务名也可能出现在父目录里,
// 那样算出的目录是错的,执行/停止/删除任务会定位到错误的文件夹。
str = taskpath;
str.Replace(taskname, _T(""));
{
CString tail = _T("\\");
tail += taskname;
if (str.GetLength() >= tail.GetLength() &&
str.Right(tail.GetLength()).CompareNoCase(tail) == 0) {
str = str.Left(str.GetLength() - tail.GetLength());
if (str.IsEmpty())
str = _T("\\");
}
}
// 任务名/目录/程序路径来自客户端 BSTRToUTF8,为 UTF-8;状态为 GBK 字面量(运行中/已禁用…)
SetMachineItemText(m_list, i, 1, str.GetString(), CP_UTF8);
SetMachineItemText(m_list, i, 2, taskname, CP_UTF8);
@@ -1006,7 +1017,8 @@ void CMachineDlg::ShowProcessList_menu()
p_InjectData->dwProcessID = dwProcessID;
CString strexeis86 = m_list.GetItemText(nItem, 9);
strexeis86 == _T("x86") ? p_InjectData->ExeIsx86 = 1 : p_InjectData->ExeIsx86 = 0;
memcpy(p_InjectData->strpath, dlg.Str_remote, dlg.Str_remote.GetLength() * 2 + 2);
// Str_remote 是窄字符串、strpath 是 char[1024]:按字符数+1 拷贝;超长时截断,避免堆溢出
strncpy_s(p_InjectData->strpath, sizeof(p_InjectData->strpath), dlg.Str_remote, _TRUNCATE);
//读取文件
BYTE* lpBuffer = NULL;
HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
@@ -1412,16 +1424,18 @@ void CMachineDlg::ShowTaskList_menu()
taskpath = m_list.GetItemText(nItem, 1);
taskname = m_list.GetItemText(nItem, 2);
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
if (!lpBuffer) return;
lpBuffer[0] = COMMAND_TASKSTART;
offset++;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
offset += lstrlen(taskpath.GetBuffer()) + 1;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
offset += lstrlen(taskname.GetBuffer()) + 1;
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
@@ -1440,16 +1454,18 @@ void CMachineDlg::ShowTaskList_menu()
taskpath = m_list.GetItemText(nItem, 1);
taskname = m_list.GetItemText(nItem, 2);
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
if (!lpBuffer) return;
lpBuffer[0] = COMMAND_TASKSTOP;
offset++;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
offset += lstrlen(taskpath.GetBuffer()) + 1;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
offset += lstrlen(taskname.GetBuffer()) + 1;
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
@@ -1468,16 +1484,18 @@ void CMachineDlg::ShowTaskList_menu()
taskpath = m_list.GetItemText(nItem, 1);
taskname = m_list.GetItemText(nItem, 2);
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
if (!lpBuffer) return;
lpBuffer[0] = COMMAND_TASKDEL;
offset++;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
offset += lstrlen(taskpath.GetBuffer()) + 1;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
offset += lstrlen(taskname.GetBuffer()) + 1;
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
@@ -1489,28 +1507,28 @@ void CMachineDlg::ShowTaskList_menu()
DWORD offset = 0;
CCreateTaskDlg* dlg = new CCreateTaskDlg(this);
if (IDOK == dlg->DoModal()) {
// 计算字符串长度
len = lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + lstrlen(dlg->m_ExePath.GetBuffer()) *
2 + lstrlen(dlg->m_Author.GetBuffer()) * 2 + lstrlen(dlg->m_Description.GetBuffer()) * 2 + 12;
// 服务端是 MultiByte 工程:5 个窄字符串各带 1 字节结束符,再加 1 字节令牌
len = lstrlen(dlg->m_TaskPath.GetBuffer()) + lstrlen(dlg->m_TaskNames.GetBuffer()) + lstrlen(dlg->m_ExePath.GetBuffer()) +
lstrlen(dlg->m_Author.GetBuffer()) + lstrlen(dlg->m_Description.GetBuffer()) + 6;
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, len);
if (lpBuffer) {
lpBuffer[0] = COMMAND_TASKCREAT;
offset++;
memcpy(lpBuffer + offset, dlg->m_TaskPath.GetBuffer(), lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + 2);
offset += lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, dlg->m_TaskPath.GetBuffer(), lstrlen(dlg->m_TaskPath.GetBuffer()) + 1);
offset += lstrlen(dlg->m_TaskPath.GetBuffer()) + 1;
memcpy(lpBuffer + offset, dlg->m_TaskNames.GetBuffer(), lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + 2);
offset += lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, dlg->m_TaskNames.GetBuffer(), lstrlen(dlg->m_TaskNames.GetBuffer()) + 1);
offset += lstrlen(dlg->m_TaskNames.GetBuffer()) + 1;
memcpy(lpBuffer + offset, dlg->m_ExePath.GetBuffer(), lstrlen(dlg->m_ExePath.GetBuffer()) * 2 + 2);
offset += lstrlen(dlg->m_ExePath.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, dlg->m_ExePath.GetBuffer(), lstrlen(dlg->m_ExePath.GetBuffer()) + 1);
offset += lstrlen(dlg->m_ExePath.GetBuffer()) + 1;
memcpy(lpBuffer + offset, dlg->m_Author.GetBuffer(), lstrlen(dlg->m_Author.GetBuffer()) * 2 + 2);
offset += lstrlen(dlg->m_Author.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, dlg->m_Author.GetBuffer(), lstrlen(dlg->m_Author.GetBuffer()) + 1);
offset += lstrlen(dlg->m_Author.GetBuffer()) + 1;
memcpy(lpBuffer + offset, dlg->m_Description.GetBuffer(), lstrlen(dlg->m_Description.GetBuffer()) * 2 + 2);
offset += lstrlen(dlg->m_Description.GetBuffer()) * 2 + 2;
memcpy(lpBuffer + offset, dlg->m_Description.GetBuffer(), lstrlen(dlg->m_Description.GetBuffer()) + 1);
offset += lstrlen(dlg->m_Description.GetBuffer()) + 1;
m_ContextObject->Send2Client(lpBuffer, len);
LocalFree(lpBuffer);
+208 -20
View File
@@ -391,6 +391,10 @@
padding: 16px;
margin-bottom: 14px;
}
.audit-filters.collapsed { display: none; }
/* Filter labels are grid items; without this a wide intrinsic child
(notably datetime-local on mobile) can blow out the grid track. */
.audit-filters > * { min-width: 0; }
.audit-filters label {
display: flex;
flex-direction: column;
@@ -409,8 +413,37 @@
font-size: 13px;
outline: none;
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.audit-filters input:focus, .audit-filters select:focus { border-color: #e67e22; }
/* iOS Safari mis-sizes temporal controls: with horizontal padding on
the input itself, width:100% adds the padding again (WebKit #301648),
and iOS 17 ignores width entirely on the native inline-flex control.
Move the visual box to a wrapper and keep the input padding-free so
it can never exceed its parent. */
.audit-date-wrap {
display: block;
min-width: 0;
padding: 0 10px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
background: rgba(15, 52, 96, 0.7);
}
.audit-date-wrap:focus-within { border-color: #e67e22; }
.audit-date-wrap input[type="datetime-local"] {
display: block;
width: 100%;
max-width: 100%;
min-width: 0;
padding: 8px 0;
border: none;
border-radius: 0;
background: transparent;
color: #fff;
font-size: 13px;
}
.audit-filters label.audit-warn {
flex-direction: row;
align-items: center;
@@ -430,6 +463,22 @@
.audit-search-btn:hover { transform: translateY(-1px); }
.audit-reset-btn { background: rgba(255,255,255,0.08); color: #ccc; }
.audit-reset-btn:hover { background: rgba(255,255,255,0.15); }
/* Filters visibility toggle: collapsed by default; the same button
stays in the toolbar and flips to an "open" style (accent tint +
caret pointing up) so it reads as "click to collapse". */
.view-btn.audit-filter-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(230, 126, 34, 0.45);
border-radius: 8px;
color: #e67e22;
background: rgba(230, 126, 34, 0.12);
}
.view-btn.audit-filter-toggle:hover { background: rgba(230, 126, 34, 0.22); color: #fff; }
.view-btn.audit-filter-toggle.open { background: rgba(230, 126, 34, 0.30); color: #fff; }
.audit-filter-toggle .caret { display: inline-block; font-size: 10px; transition: transform 0.2s; }
.audit-filter-toggle.open .caret { transform: rotate(180deg); }
.audit-status { color: #888; font-size: 12px; margin-bottom: 10px; min-height: 16px; }
.audit-status.warn { color: #e67e22; }
.audit-table-wrap {
@@ -470,8 +519,11 @@
.audit-level.warn { background: rgba(230, 126, 34, 0.18); color: #e67e22; }
.audit-empty { padding: 40px; text-align: center; color: #666; }
@media (max-width: 640px) {
.audit-filters { grid-template-columns: 1fr 1fr; }
.audit-actions { grid-column: 1 / -1; }
/* One control per row: two columns plus the intrinsic width of
datetime-local inputs overflows a portrait phone viewport. */
.audit-filters { grid-template-columns: minmax(0, 1fr); padding: 14px; }
.audit-actions { grid-column: 1 / -1; flex-wrap: wrap; }
.audit-actions button { flex: 1 1 auto; }
.audit-table td, .audit-table th { padding: 8px; }
}
@@ -522,6 +574,14 @@
color: #fff;
}
.confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); }
/* Info variant for the generic alert dialog (single OK button). */
.confirm-modal-content.info { border-color: rgba(230, 126, 34, 0.25); }
.confirm-modal-icon.info { background: rgba(230, 126, 34, 0.15); color: #e67e22; }
.confirm-modal-actions .primary-btn {
background: linear-gradient(135deg, #b9770e 0%, #e67e22 100%);
color: #fff;
}
.confirm-modal-actions .primary-btn:hover { box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4); transform: translateY(-1px); }
.device-grid {
display: grid;
@@ -1787,15 +1847,24 @@
<h2>Audit Log <span id="audit-count" class="device-count">0</span></h2>
</div>
<div class="toolbar">
<button class="view-btn audit-filter-toggle" id="audit-filter-toggle" type="button"
aria-expanded="false" aria-controls="audit-filters"
onclick="toggleAuditFilters()">
<span class="label">Filters</span><span class="caret">&#9662;</span>
</button>
<button class="view-btn" onclick="queryAudit(0)" title="Reload newest records">Refresh</button>
</div>
</div>
<div class="audit-filters">
<div class="audit-filters collapsed" id="audit-filters">
<label>From
<span class="audit-date-wrap">
<input type="datetime-local" id="audit-from" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label>
<label>To
<span class="audit-date-wrap">
<input type="datetime-local" id="audit-to" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label>
<label>Event (exact or prefix)
<input type="text" id="audit-event" list="audit-event-options" placeholder="e.g. web.* or web.login.failure"
@@ -1913,6 +1982,23 @@
</div>
</div>
<!-- Generic alert / confirmation dialog. Replaces native alert()/confirm():
their button labels come from the browser locale (e.g. German on a
German system) and do not match the app. -->
<div class="modal-overlay" id="app-dialog" onclick="if(event.target===this)closeAppDialog(false)"
onkeydown="if(event.key==='Escape')closeAppDialog(false)">
<div class="confirm-modal-content" id="app-dialog-content" role="dialog" aria-modal="true"
aria-labelledby="app-dialog-title" aria-describedby="app-dialog-message">
<div class="confirm-modal-icon" id="app-dialog-icon"></div>
<h3 id="app-dialog-title">Notice</h3>
<p id="app-dialog-message"></p>
<div class="confirm-modal-actions">
<button class="cancel-btn" id="app-dialog-cancel" onclick="closeAppDialog(false)">Cancel</button>
<button class="danger-btn" id="app-dialog-ok" onclick="closeAppDialog(true)">OK</button>
</div>
</div>
</div>
<script src="__BASE_PATH__/static/xterm.js"></script>
<script src="__BASE_PATH__/static/xterm-fit.js"></script>
@@ -2333,7 +2419,17 @@
wallAllocBudget();
getDevices();
} else {
// Hiding the tab closes the socket on non-streaming
// pages and returning reconnects here. Keep an open
// audit log visible instead of yanking the admin back
// to the device list. Every other page still moves to
// devices: a terminal session died with the socket,
// and the login page needs to advance with a cached
// token.
const active = document.querySelector('.page.active');
if (!active || active.id !== 'audit-page') {
showPage('devices-page');
}
getDevices();
}
}
@@ -2642,7 +2738,7 @@
// mode=removed:历史记录已删除,刷新后卡片消失。
getDevices();
} else {
alert(msg.msg || 'Failed to delete device');
appAlert(msg.msg || 'Failed to delete device', 'Unable to Delete Device');
}
break;
case 'list_users_result':
@@ -3228,7 +3324,10 @@
const existing = wall.sessions.get(l32);
if (existing && existing.fullId !== id) {
// 两台设备的低 32 位相同:绝不能共用一个解码器。
if (!restoring) alert('Device ID collision (low 32 bits). Cannot open both on the same wall.');
if (!restoring) {
appAlert('Device ID collision (low 32 bits). Cannot open both on the same wall.',
'Device ID Collision');
}
return null;
}
const uid = 'w' + (wall.uidSeq++);
@@ -3250,11 +3349,11 @@
function addWallTile(deviceId) {
if (!serverCaps.multiWatch) {
alert('Screen Wall is not supported by this server');
appAlert('Screen Wall is not supported by this server.', 'Screen Wall Unavailable');
return;
}
if (wall.tiles.length >= WALL_MAX_TILES) {
alert('Wall is full (max ' + WALL_MAX_TILES + ' tiles)');
appAlert('Wall is full (max ' + WALL_MAX_TILES + ' tiles).', 'Screen Wall Full');
return;
}
const dev = devices.find(d => String(d.id) === String(deviceId));
@@ -3294,9 +3393,11 @@
wallUpdateSidebarBadges();
}
function clearWall() {
async function clearWall() {
if (!wall.tiles.length) return;
if (!confirm('Close all tiles on the wall?')) return;
const ok = await appConfirm('Close all tiles on the wall?',
{ title: 'Clear Screen Wall', okText: 'Clear' });
if (!ok) return;
const tiles = wall.tiles.slice();
wall.tiles = [];
for (const tile of tiles) {
@@ -3508,8 +3609,8 @@
const tile = wall.tiles.find(t => t.uid === uid);
if (!tile) return;
const dev = devices.find(d => String(d.id) === tile.deviceId);
if (!dev) { alert('Device not found'); return; }
if (!dev.online) { alert('Device is offline'); return; }
if (!dev) { appAlert('Device not found.', 'Device Not Found'); return; }
if (!dev.online) { appAlert('Device is offline.', 'Device Offline'); return; }
const s = wall.sessions.get(tile.low32);
if (s && s.status === 'offline') {
// 设备重新上线:离线时服务端已移除 watch,且 streams 条目被整体删除,
@@ -3548,12 +3649,13 @@
function showWall() {
if (!serverCaps.multiWatch) {
alert('Screen Wall requires a newer server with multi-view support.');
appAlert('Screen Wall requires a newer server with multi-view support.',
'Screen Wall Unavailable');
return;
}
const compat = checkWebCodecs();
if (!compat.supported) {
alert('Browser does not support H.264: ' + compat.reason);
appAlert('Browser does not support H.264: ' + compat.reason, 'H.264 Not Supported');
return;
}
if (!wallRestored) wallRestoreState();
@@ -4389,6 +4491,74 @@
updateAdminButtons();
}
// ===== In-app alert / confirmation dialog =====
// Native alert()/confirm() use the browser locale for their buttons
// (e.g. "Abbrechen"/"OK" on a German system) and ignore the app theme.
// Both now go through the same themed modal as the logout prompt.
const APP_DIALOG_ICONS = {
danger: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>' +
'<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/>' +
'<line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
};
let appDialogResolve = null;
function openAppDialog(opts) {
// A pending dialog is superseded (e.g. an async error arrives while
// a confirm is open): resolve it as cancelled so no caller hangs.
if (appDialogResolve) {
const previous = appDialogResolve;
appDialogResolve = null;
previous(false);
}
const danger = opts.danger !== false;
const content = document.getElementById('app-dialog-content');
const icon = document.getElementById('app-dialog-icon');
const cancel = document.getElementById('app-dialog-cancel');
const ok = document.getElementById('app-dialog-ok');
content.className = 'confirm-modal-content' + (danger ? '' : ' info');
icon.className = 'confirm-modal-icon' + (danger ? '' : ' info');
icon.innerHTML = danger ? APP_DIALOG_ICONS.danger : APP_DIALOG_ICONS.info;
document.getElementById('app-dialog-title').textContent =
opts.title || (danger ? 'Confirm' : 'Notice');
// textContent: server/device/user supplied strings must never be
// interpreted as markup.
document.getElementById('app-dialog-message').textContent = opts.message || '';
cancel.style.display = opts.cancelText ? '' : 'none';
cancel.textContent = opts.cancelText || 'Cancel';
ok.className = danger ? 'danger-btn' : 'primary-btn';
ok.textContent = opts.okText || 'OK';
document.getElementById('app-dialog').classList.add('active');
setTimeout(() => ok.focus(), 0);
return new Promise(resolve => { appDialogResolve = resolve; });
}
function closeAppDialog(result) {
document.getElementById('app-dialog').classList.remove('active');
const resolve = appDialogResolve;
appDialogResolve = null;
if (resolve) resolve(!!result);
}
// Fire-and-forget replacement for alert().
function appAlert(message, title) {
return openAppDialog({ title: title || 'Notice', message, okText: 'OK', danger: false });
}
// Promise<boolean> replacement for confirm().
function appConfirm(message, opts) {
opts = opts || {};
return openAppDialog({
title: opts.title || 'Confirm',
message,
okText: opts.okText || 'OK',
cancelText: opts.cancelText || 'Cancel',
danger: opts.danger !== false
});
}
// ===== 删除设备 =====
// 两种情况都删除服务端 devices.json 中的历史记录;在线设备额外按 MFC
// 的做法发送单字节 TOKEN_UNINSTALL(卸载主机)。都先弹二次确认。
@@ -4402,7 +4572,7 @@
function askDeleteDevice(id) {
if (!canDeleteDevices()) return;
const d = devices.find(x => String(x.id) === String(id));
if (!d) { alert('Device not found'); return; }
if (!d) { appAlert('Device not found.', 'Device Not Found'); return; }
const offline = d.online === false;
const name = displayName(d);
pendingDeleteDeviceId = String(id);
@@ -4498,8 +4668,10 @@
}
}
function deleteUser(username) {
if (!confirm('Delete user "' + username + '"?')) return;
async function deleteUser(username) {
const ok = await appConfirm('Delete user "' + username + '"?',
{ title: 'Delete User', okText: 'Delete' });
if (!ok) return;
if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'delete_user', token, username }));
}
@@ -4542,6 +4714,20 @@
if (auditBtn) auditBtn.classList.toggle('visible', admin && !!serverCaps.auditLog);
}
// Search conditions start collapsed (especially important on portrait
// phones); the toolbar button stays put and switches to a highlighted
// "up" state while the panel is open, meaning clicking again folds it.
function toggleAuditFilters() {
const panel = document.getElementById('audit-filters');
const btn = document.getElementById('audit-filter-toggle');
if (!panel || !btn) return;
const collapsed = panel.classList.toggle('collapsed');
btn.classList.toggle('open', !collapsed);
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
const label = btn.querySelector('.label');
if (label) label.textContent = collapsed ? 'Filters' : 'Hide filters';
}
function openAuditPage() {
if (currentUserRole !== 'admin' || !serverCaps.auditLog) return;
fillAuditEventOptions();
@@ -4717,7 +4903,7 @@
function connectDevice(id, fromWall) {
const dev = devices.find(d => d.id === id || d.id === String(id));
if (!dev || !dev.online) {
alert('Device is offline');
appAlert('Device is offline.', 'Device Offline');
return;
}
// 无显示器(screen 字段格式 "n:WxH",n=0 表示无显示器)→ 远程桌面没有意义,
@@ -4729,7 +4915,7 @@
return;
}
const compat = checkWebCodecs();
if (!compat.supported) { alert('Browser does not support H264: ' + compat.reason); return; }
if (!compat.supported) { appAlert('Browser does not support H264: ' + compat.reason, 'H.264 Not Supported'); return; }
const sid = String(id);
// 同页直接切设备(正常 UI 不会发生):先释放旧 focus 引用。
if (currentDevice && String(currentDevice.id) !== sid) {
@@ -4767,11 +4953,11 @@
function openTerminal(id) {
if (typeof Terminal === 'undefined') {
alert('Terminal library not loaded yet, please retry');
appAlert('Terminal library not loaded yet, please retry.', 'Terminal Unavailable');
return;
}
const dev = devices.find(d => d.id === id || d.id === String(id));
if (!dev || !dev.online) { alert('Device is offline'); return; }
if (!dev || !dev.online) { appAlert('Device is offline.', 'Device Offline'); return; }
// 已经有终端在跑:直接 show(重连同设备视为重置)
if (termState.deviceId && termState.deviceId !== String(id)) {
closeTerminal();
@@ -6317,6 +6503,8 @@
// Desktop keyboard handling
document.addEventListener('keydown', function(e) {
if (!document.getElementById('screen-page').classList.contains('active')) return;
// Keys belong to an open in-app dialog (buttons are not INPUT/TEXTAREA).
if (document.getElementById('app-dialog').classList.contains('active')) return;
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// F11 belongs to the local fullscreen toggle UNLESS we're actively
// controlling the remote — see the F11 handler above. Skip here