Compare commits
11
Commits
ca671d6453
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38cd207f5d | ||
|
|
6c8336b497 | ||
|
|
c208b7368e | ||
|
|
15a50db0a7 | ||
|
|
ca35e6e26f | ||
|
|
679136e73e | ||
|
|
b637c94e24 | ||
|
|
f76cd79bb0 | ||
|
|
39c8daa9b0 | ||
|
|
cd4f3675fd | ||
|
|
bbb3d8c2fa |
No files matched your search
@@ -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 扩展 & 国际化**
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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 擴充 & 國際化**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
//拷贝数据
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Binary file not shown.
+22
-4
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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` 一致。
|
||||
|
||||
|
||||
@@ -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.
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 格式图片");
|
||||
|
||||
@@ -107,4 +107,5 @@ public:
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
|
||||
void EnableWindowsOnlyControls(BOOL enable);
|
||||
CComboBox m_ComboLocation;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 末尾)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.4.0"
|
||||
#define BRAND_VERSION "1.4.1"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
@@ -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">▾</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
|
||||
|
||||
Reference in new issue
Block a user