Compare commits
50
Commits
59fead7b5f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38cd207f5d | ||
|
|
6c8336b497 | ||
|
|
c208b7368e | ||
|
|
15a50db0a7 | ||
|
|
ca35e6e26f | ||
|
|
679136e73e | ||
|
|
b637c94e24 | ||
|
|
f76cd79bb0 | ||
|
|
39c8daa9b0 | ||
|
|
cd4f3675fd | ||
|
|
bbb3d8c2fa | ||
|
|
ca671d6453 | ||
|
|
a522424491 | ||
|
|
b1ddec1b8a | ||
|
|
1922f87dd2 | ||
|
|
8eab26c1f6 | ||
|
|
04f4eb9947 | ||
|
|
5cea1167be | ||
|
|
0c04f7516b | ||
|
|
e6b4fe3a85 | ||
|
|
d3a339247b | ||
|
|
33986f4893 | ||
|
|
e5f5720867 | ||
|
|
d7b49a3d06 | ||
|
|
c85039ea0a | ||
|
|
a447452521 | ||
|
|
614b03994c | ||
|
|
99e3bcbc6d | ||
|
|
8f02e73b9c | ||
|
|
7651ee8602 | ||
|
|
4ff965a2b4 | ||
|
|
77b688c981 | ||
|
|
c93f09e9ac | ||
|
|
96b9eea70a | ||
|
|
ca0d7626d2 | ||
|
|
8d4c118ae6 | ||
|
|
d477cbfee9 | ||
|
|
870a5832ea | ||
|
|
8e0f1283c6 | ||
|
|
2de21b2b6b | ||
|
|
53b7151fb7 | ||
|
|
d2149851b2 | ||
|
|
ca6f76565c | ||
|
|
d15cd77a28 | ||
|
|
1a9312d2d1 | ||
|
|
f3a8406050 | ||
|
|
ff6b290580 | ||
|
|
da0ec983ba | ||
|
|
6220335f63 | ||
|
|
c7f7b7b29b |
No files matched your search
@@ -93,6 +93,7 @@ Bin/*
|
||||
nul
|
||||
server/go/web/assets/index.html
|
||||
server/go/users.json
|
||||
server/go/devices.json
|
||||
server/go/build/
|
||||
server/go/.claude/settings.json
|
||||
android/app/.cxx/
|
||||
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.4.1 (2026.10.20)
|
||||
|
||||
**Web 运维控制台 & 安全加固 & 单向 TLS**
|
||||
|
||||
**新功能:**
|
||||
- **Web 屏幕墙**:同一标签页以 1×1 ~ 5×5 布局同时监控多台设备,桌面 9 路 / 触控 4 路实时解码,超出预算暂停并自动晋升;点击 tile 进入单画面控制;同一设备可重复添加并共享一路流;MFC 与 Go 服务端同步支持(旧前端 / 旧服务端自动降级)
|
||||
- **安全审计日志**:登录 / 授权拒绝 / WS 生命周期 / 屏幕与终端会话 / 剪贴板(仅字节数)/ 设备认证 / 管理变更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 条内存环形缓冲;Web 新增审计日志查询页(时间 / 事件 / 操作者 / IP / 设备 / 关键字 / 告警过滤 + 分页)
|
||||
- **离线设备历史(Go)**:`devices.json` 持久化所有登录过的设备,设备列表可按需显示离线记录;管理员可删除记录或对在线设备执行远程卸载
|
||||
- **单向 TLS 传输**:设备端口同一端口嗅探 ClientHello,TLS 1.2 客户端加密、旧明文客户端不受影响;MFC「扩展 > TLS证书」运行时配置;在线列表新增 TLS 列,右键可远程启用 / 取消(客户端下次启动生效)
|
||||
- **Web 远程桌面增强**:Ctrl+C/X/V 双向文本剪贴板同步、手机「I」输入框(经剪贴板桥注入)、登录会话跨标签页共享、管理员 SSO 桥接
|
||||
- **客户端安装位置选择**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
|
||||
- **Go Web UI 白标**:`YAMA_WEB_BRAND_NAME` 替换产品名并移除安全提示与外链
|
||||
- **安全加固**:每 IP 连接与登录限流、单连接入站缓冲上限、屏幕 / 终端子连接绑定源 IP、可选 ConnAuth HMAC 校验
|
||||
- **克隆机 ID 去重**:V2 `client_id` 折叠主机名,撞 ID 时服务端下发 `COMMAND_REGEN_CLIENT_ID` 让客户端重算重连
|
||||
|
||||
**改进:**
|
||||
- 文件传输速度改由 MasterSettings 下发(修复重复弹窗),传输对话框标题实时显示速度
|
||||
- 客户端聊天关闭仅隐藏;日志右键新增白名单;MCP 勾选即时刷新;Web 原生 alert/confirm 全部替换为主题化应用内对话框
|
||||
- 非中文系统统一按 GBK / UTF-8 解码(对话框 / 进程列表 / 文件列表 / 路径 / 地理位置 / 主机管理 / 剪贴板)
|
||||
|
||||
**Bug 修复:**
|
||||
- 共享 / 转移主机后重启主控不重连;分享 / 转移权限误判
|
||||
- 已过期授权续期从今天起算,提示与日志显示实际到期日
|
||||
- V2 上传快捷目录未展开 `%USERPROFILE%`;无摄像头主机发送 0×0 画面信息;列切换残留像素;Go 设备端口日志刷屏;审计页最小化后跳回设备页
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
|
||||
|
||||
@@ -378,6 +378,31 @@ Valid : 2026-02-01 to 2028-02-01
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.4.1 (2026.10.20)
|
||||
|
||||
**Web operations console & security hardening & one-way TLS**
|
||||
|
||||
**New features:**
|
||||
- **Web screen wall**: monitor several devices in one tab with 1x1 to 5x5 layouts; desktop decodes up to 9 live streams and touch devices 4, tiles over budget show Paused and are promoted automatically; click a tile for single-screen control; the same device can be added repeatedly and shares one stream; supported by both the MFC and Go servers (old frontends / old servers degrade automatically)
|
||||
- **Security audit log**: login attempts, authorization denials, WebSocket lifecycle, screen / terminal sessions, clipboard (byte counts only), device authentication and admin mutations; the Go server persists to `logs/audit.log`, the MFC server keeps the newest 1000 events in an in-memory ring; new Audit Log page with time / event / actor / IP / device / keyword / warn filters and paging
|
||||
- **Offline device history (Go)**: `devices.json` persists every device that has ever logged in, and the device list can show offline records; admins can delete a record or remotely uninstall an online device
|
||||
- **One-way TLS transport**: the device port sniffs ClientHello on the same port, so TLS 1.2 clients are encrypted while legacy plaintext clients keep working; "Extensions > TLS Certificate" configures the MFC server at runtime; a TLS column and a per-client remote enable / disable action (effective on the client's next start)
|
||||
- **Web remote desktop enhancements**: bidirectional Ctrl+C/X/V text clipboard sync, a phone "I" input box that injects text through the clipboard bridge, login sessions shared across tabs, and an admin-only SSO bridge
|
||||
- **Client install location**: ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
|
||||
- **White-label Go web UI**: `YAMA_WEB_BRAND_NAME` replaces the product name and removes the security notice and external links
|
||||
- **Security hardening**: per-IP connection and login rate limits, a per-connection inbound buffer cap, source-IP binding for screen / terminal sub-connections, and optional ConnAuth HMAC verification
|
||||
- **Clone ID deduplication**: the V2 `client_id` folds in the hostname; on a collision the server sends `COMMAND_REGEN_CLIENT_ID` so the client recomputes and reconnects
|
||||
|
||||
**Improvements:**
|
||||
- File-transfer speed is delivered via MasterSettings (fixes duplicate dialogs), and the transfer dialog title shows live speed
|
||||
- Closing the client chat now hides it; whitelist action in the log context menu; MCP checkmark refreshes immediately; native browser alert/confirm replaced with themed in-app dialogs
|
||||
- GBK / UTF-8 decoding across non-Chinese systems (dialogs, process list, file lists, paths, geolocation, machine management, clipboard)
|
||||
|
||||
**Bug fixes:**
|
||||
- Hosts did not reconnect after a master restart following share / transfer; share / transfer permission misjudgment
|
||||
- Expired licenses now renew from today, and prompts and logs show the actually issued date
|
||||
- V2 quick-folder uploads did not expand `%USERPROFILE%`; webcam-less hosts sent 0x0 bitmap info; stale pixels after column toggles; Go device-port log flood; audit page reset after minimizing the browser
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
|
||||
|
||||
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### v1.4.1 (2026.10.20)
|
||||
|
||||
**Web 維運控制台 & 安全加固 & 單向 TLS**
|
||||
|
||||
**新功能:**
|
||||
- **Web 螢幕牆**:同一分頁以 1×1 ~ 5×5 佈局同時監控多台裝置,桌面 9 路 / 觸控 4 路即時解碼,超出預算暫停並自動晉升;點擊 tile 進入單畫面控制;同一裝置可重複加入並共享一路流;MFC 與 Go 伺服端同步支援(舊前端 / 舊伺服端自動降級)
|
||||
- **安全稽核日誌**:登入 / 授權拒絕 / WS 生命週期 / 螢幕與終端工作階段 / 剪貼簿(僅位元組數)/ 裝置認證 / 管理變更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 條記憶體環形緩衝;Web 新增稽核日誌查詢頁(時間 / 事件 / 操作者 / IP / 裝置 / 關鍵字 / 告警過濾 + 分頁)
|
||||
- **離線裝置歷史(Go)**:`devices.json` 持久化所有登入過的裝置,裝置列表可依需顯示離線記錄;管理員可刪除記錄或對線上裝置執行遠端解除安裝
|
||||
- **單向 TLS 傳輸**:裝置連接埠同一連接埠嗅探 ClientHello,TLS 1.2 用戶端加密、舊明文用戶端不受影響;MFC「擴充 > TLS憑證」執行時設定;線上列表新增 TLS 欄,右鍵可遠端啟用 / 取消(用戶端下次啟動生效)
|
||||
- **Web 遠端桌面增強**:Ctrl+C/X/V 雙向文字剪貼簿同步、手機「I」輸入框(經剪貼簿橋注入)、登入工作階段跨分頁共享、管理員 SSO 橋接
|
||||
- **用戶端安裝位置選擇**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
|
||||
- **Go Web UI 白標**:`YAMA_WEB_BRAND_NAME` 取代產品名稱並移除安全提示與外部連結
|
||||
- **安全加固**:每 IP 連線與登入限流、單連線入站緩衝上限、螢幕 / 終端子連線綁定來源 IP、可選 ConnAuth HMAC 校驗
|
||||
- **複製機 ID 去重**:V2 `client_id` 納入主機名稱,撞 ID 時伺服端下發 `COMMAND_REGEN_CLIENT_ID` 讓用戶端重算重連
|
||||
|
||||
**改進:**
|
||||
- 檔案傳輸速度改由 MasterSettings 下發(修復重複彈窗),傳輸對話框標題即時顯示速度
|
||||
- 用戶端聊天關閉僅隱藏;日誌右鍵新增白名單;MCP 勾選即時重新整理;Web 原生 alert/confirm 全部替換為主題化應用內對話框
|
||||
- 非中文系統統一按 GBK / UTF-8 解碼(對話框 / 處理程序列表 / 檔案列表 / 路徑 / 地理位置 / 主機管理 / 剪貼簿)
|
||||
|
||||
**Bug 修復:**
|
||||
- 分享 / 轉移主機後重啟主控不重連;分享 / 轉移權限誤判
|
||||
- 已過期授權續期從今天起算,提示與日誌顯示實際到期日
|
||||
- V2 上傳快捷目錄未展開 `%USERPROFILE%`;無攝影機主機傳送 0×0 畫面資訊;欄切換殘留像素;Go 裝置連接埠日誌刷屏;稽核頁最小化後跳回裝置頁
|
||||
|
||||
### v1.4.0 (2026.10.7)
|
||||
|
||||
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -245,14 +245,17 @@ INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wPara
|
||||
return TRUE;
|
||||
}
|
||||
if (LOWORD(wParam) == IDC_BUTTON_END) {
|
||||
// 被控端主动结束交谈:关闭本地聊天窗口
|
||||
SendMessage(hDlg, WM_USER + 1, NULL, NULL);
|
||||
// 被控端关闭聊天窗口:仅隐藏对话框,会话保持。关闭交谈始终由服务端发起,
|
||||
// 服务端继续发消息时 OnReceive 会用 SW_RESTORE 重新弹出本对话框。
|
||||
ShowWindow(hDlg, SW_HIDE);
|
||||
return TRUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CLOSE:
|
||||
return 0;
|
||||
// X 关闭按钮:仅隐藏对话框(与「结束交谈」一致),关闭交谈始终由服务端发起
|
||||
ShowWindow(hDlg, SW_HIDE);
|
||||
return TRUE;
|
||||
|
||||
case WM_USER + 1:
|
||||
pThis->m_hWnd = NULL;
|
||||
|
||||
+25
-15
@@ -283,7 +283,7 @@ int main(int argc, const char *argv[])
|
||||
int r = RegisterStartup(
|
||||
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
|
||||
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
|
||||
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
|
||||
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf, g_SETTINGS.installFlag);
|
||||
if (r <= 0) {
|
||||
BOOL s = self_del();
|
||||
if (!IsDebug) {
|
||||
@@ -445,6 +445,11 @@ extern "C" __declspec(dllexport) void StopRun()
|
||||
// 是否成功停止
|
||||
extern "C" __declspec(dllexport) bool IsStoped()
|
||||
{
|
||||
// 主控端重启(S_SERVER_EXIT):主实例工作线程已结束,即使分享实例仍在运行,
|
||||
// 也视为「可重连」。旧 TestRun 加载循环靠它立即重连,无需卸载/重装 TestRun.exe;
|
||||
// 分享实例继续存活,不受主控端重启影响。
|
||||
if (S_SERVER_EXIT == g_MyApp.g_bExit)
|
||||
return true;
|
||||
return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0;
|
||||
}
|
||||
|
||||
@@ -462,14 +467,12 @@ extern "C" __declspec(dllexport) int EasyRun()
|
||||
|
||||
do {
|
||||
TestRun((char*)settings.ServerIP(), settings.ServerPort());
|
||||
if (S_SERVER_EXIT == app.g_bExit)
|
||||
continue; // 主控端重启:立即重连,不等仍存活的分享实例
|
||||
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净再退出
|
||||
while (!IsStoped())
|
||||
Sleep(50);
|
||||
if (S_CLIENT_EXIT == app.g_bExit) // 受控端退出
|
||||
break;
|
||||
else if (S_SERVER_EXIT == app.g_bExit)
|
||||
continue;
|
||||
else // S_CLIENT_UPDATE: 程序更新
|
||||
break;
|
||||
break;
|
||||
} while (true);
|
||||
|
||||
return app.g_bExit;
|
||||
@@ -555,14 +558,14 @@ extern "C" __declspec(dllexport) void Run(HWND hwnd, HINSTANCE hinst, LPSTR lpsz
|
||||
|
||||
do {
|
||||
TestRun((char*)result[0].c_str(), atoi(result[1].c_str()));
|
||||
if (bExit == S_SERVER_EXIT)
|
||||
continue; // 主控端重启:立即重连,不等仍存活的分享实例
|
||||
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净
|
||||
while (!IsStoped())
|
||||
Sleep(20);
|
||||
if (bExit == S_CLIENT_EXIT)
|
||||
return;
|
||||
else if (bExit == S_SERVER_EXIT)
|
||||
continue;
|
||||
else // S_CLIENT_UPDATE
|
||||
break;
|
||||
break; // S_CLIENT_UPDATE
|
||||
} while (true);
|
||||
|
||||
sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort());
|
||||
@@ -608,10 +611,17 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
}
|
||||
auto list = app.GetSharedMasterList();
|
||||
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);
|
||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
// 并发上线的并行实例只派生一次:主控端重启重连时会再次进入 StartClient,
|
||||
// 若不守护会重复派生并行连接。主实例的 SetServer(list[0]) 保持每次执行,
|
||||
// 让主实例始终指向域名解析出的第一个 IP。
|
||||
static bool parallelHasRun = false;
|
||||
if (!parallelHasRun) {
|
||||
parallelHasRun = true;
|
||||
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, FALSE);
|
||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
}
|
||||
}
|
||||
// The main ClientApp.
|
||||
settings.SetServer(list[0].c_str(), settings.ServerPort());
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
<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>
|
||||
@@ -264,6 +265,7 @@
|
||||
<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" />
|
||||
|
||||
@@ -46,6 +46,16 @@
|
||||
<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" />
|
||||
@@ -99,6 +109,19 @@
|
||||
<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" />
|
||||
|
||||
@@ -60,19 +60,20 @@ static void ResolveProcName(DWORD pid, char* out, int outLen)
|
||||
out[outLen - 1] = 0;
|
||||
}
|
||||
|
||||
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
|
||||
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
|
||||
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是把网络序地址直接放在 DWORD 的**内存**里,
|
||||
// 不是主机序数值(192.168.0.55 在 x86 上 DWORD 数值是 0x3700A8C0、内存字节是 C0 A8 00 37)。
|
||||
// 所以只能按内存拷贝:这样得到的正是 inet_ntop 需要的网络序字节,也能原样拼回 SetTcpEntry 的 DWORD。
|
||||
// 按数值移位会把字节序弄反(显示成 55.0.168.192)。
|
||||
static void PutAddr4(uint8_t* out, DWORD addrNet)
|
||||
{
|
||||
out[0] = (uint8_t)(addrNet >> 24);
|
||||
out[1] = (uint8_t)(addrNet >> 16);
|
||||
out[2] = (uint8_t)(addrNet >> 8);
|
||||
out[3] = (uint8_t)(addrNet);
|
||||
memcpy(out, &addrNet, 4);
|
||||
}
|
||||
|
||||
static DWORD GetAddr4(const uint8_t* addr)
|
||||
{
|
||||
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
|
||||
DWORD addrNet = 0;
|
||||
memcpy(&addrNet, addr, 4);
|
||||
return addrNet;
|
||||
}
|
||||
|
||||
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
|
||||
|
||||
+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;
|
||||
|
||||
@@ -838,6 +838,39 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_cfg->SetStr("settings", "group_name", group);
|
||||
break;
|
||||
}
|
||||
case COMMAND_REGEN_CLIENT_ID: {
|
||||
// 撞 ID 的克隆机:服务端已判定「主机名不同却撞 client_id」,让本机重生成 ID。
|
||||
// 新 ID = 旧 ID + 随机 UUID 再哈希,写回注册表后主动断开主连接,重连循环
|
||||
// 会用新 ID 重新登录,脱离撞 ID 状态(脱离替补、正常显示在主机列表)。
|
||||
uint64_t newID = RegenerateClientID(m_conn->clientID);
|
||||
m_cfg->SetStr("settings", "client_id", std::to_string(newID));
|
||||
Mprintf("[regen-client-id] 重生成 client_id: %llu -> %llu,即将重连\n",
|
||||
m_conn->clientID, newID);
|
||||
if (m_ClientObject)
|
||||
m_ClientObject->Disconnect();
|
||||
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();
|
||||
@@ -1488,7 +1521,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
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);
|
||||
|
||||
+56
-10
@@ -289,6 +289,35 @@ uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normal
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
// 撞 ID 的克隆机重生成 client_id:在旧 ID 基础上掺一个一次性随机 UUID 再哈希。
|
||||
// 只在服务端判定「主机名不同却撞 ID」后下发 COMMAND_REGEN_CLIENT_ID 触发,不影响
|
||||
// 正常(首次)登录的 CalcalateIDv2 算法;唯一性由随机 UUID 保证,同批克隆机各自
|
||||
// 随机,重算后必然不同。
|
||||
uint64_t RegenerateClientID(uint64_t oldID)
|
||||
{
|
||||
// 16 字节随机数充当一次性 UUID(RtlGenRandom = advapi32 SystemFunction036)。
|
||||
unsigned char rnd[16] = {};
|
||||
if (!RtlGenRandom(rnd, (ULONG)sizeof(rnd))) {
|
||||
// 极少数环境 RtlGenRandom 可能失败:退化为时间/进程/线程/性能计数混合的 LCG,
|
||||
// 保证仍能产出与旧 ID 不同的新 ID(不追求密码学强度,只需撞车概率足够低)。
|
||||
uint64_t mix = GetTickCount64() ^ ((uint64_t)GetCurrentProcessId() << 32)
|
||||
^ (uint64_t)GetCurrentThreadId();
|
||||
LARGE_INTEGER pc;
|
||||
QueryPerformanceCounter(&pc);
|
||||
mix ^= (uint64_t)pc.QuadPart;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
mix = mix * 6364136223846793005ULL + 1442695040888963407ULL;
|
||||
rnd[i] = (unsigned char)(mix >> 56);
|
||||
}
|
||||
}
|
||||
char hex[33];
|
||||
for (int i = 0; i < 16; ++i)
|
||||
sprintf_s(hex + i * 2, 3, "%02X", (unsigned)rnd[i]);
|
||||
hex[32] = '\0';
|
||||
std::string seed = std::to_string(oldID) + "|" + hex;
|
||||
return XXH64(seed.c_str(), seed.size(), 0);
|
||||
}
|
||||
|
||||
BOOL IsAuthKernel(std::string &str) {
|
||||
BOOL isAuthKernel = FALSE;
|
||||
std::string pid = std::to_string(GetCurrentProcessId());
|
||||
@@ -378,13 +407,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);
|
||||
cfg.SetStr("settings", "location", loc);
|
||||
cfg.SetStr("settings", "public_ip", pubIP);
|
||||
cfg.SetInt("settings", "ip_time", time(0));
|
||||
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 +441,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 +473,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
std::string GetMachineGuidWindows();
|
||||
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath, bool isAuth = false);
|
||||
uint64_t RegenerateClientID(uint64_t oldID);
|
||||
BOOL IsAuthKernel(std::string& str);
|
||||
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS &conn, const std::string& expiredDate, bool isAuth);
|
||||
DWORD CPUClockMHz();
|
||||
|
||||
+400
-131
@@ -122,6 +122,23 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
|
||||
case COMMAND_MACHINE_TASK:
|
||||
GetRoot();
|
||||
break;
|
||||
// 计划任务动作:lpBuffer+1 起是 NUL 结尾的窄字符串(服务端按 MultiByte 组包)
|
||||
case COMMAND_TASKSTART:
|
||||
RunOrStopTask(lpBuffer + 1, TRUE);
|
||||
GetRoot();
|
||||
break;
|
||||
case COMMAND_TASKSTOP:
|
||||
RunOrStopTask(lpBuffer + 1, FALSE);
|
||||
GetRoot();
|
||||
break;
|
||||
case COMMAND_TASKDEL:
|
||||
DelTask(lpBuffer + 1);
|
||||
GetRoot();
|
||||
break;
|
||||
case COMMAND_TASKCREAT:
|
||||
CreateTask(lpBuffer + 1);
|
||||
GetRoot();
|
||||
break;
|
||||
case COMMAND_MACHINE_HOSTS:
|
||||
SendHostsList();
|
||||
break;
|
||||
@@ -190,8 +207,13 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
|
||||
DebugPrivilege(SE_DEBUG_NAME, TRUE);
|
||||
char strProcessName[MAX_PATH] = { 0 };
|
||||
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *(LPDWORD)(lpBuffer + 1));
|
||||
if (hProcess == NULL) {
|
||||
DebugPrivilege(SE_DEBUG_NAME, FALSE);
|
||||
break;
|
||||
}
|
||||
GetModuleFileNameEx(hProcess, NULL, strProcessName, MAX_PATH);
|
||||
if (lpBuffer[1]) {
|
||||
// PID 的第 2~4 字节非零时也要删除:只看 lpBuffer[1] 会把低字节为 0 的 PID 漏掉
|
||||
if (*(LPDWORD)(lpBuffer + 1) != 0) {
|
||||
char Tem_path[MAX_PATH];
|
||||
char path_one[MAX_PATH];
|
||||
char path_two[MAX_PATH];
|
||||
@@ -218,10 +240,10 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
|
||||
DeleteFile(path_one); //删除目录(目录)
|
||||
Sleep(200);
|
||||
if (GetFileAttributes(strProcessName) == -1) //检查文件不存在 退出
|
||||
return;
|
||||
break;
|
||||
n++;
|
||||
if (n >= 10) //无法删除 一段时间后退出
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CloseHandle(hProcess);
|
||||
@@ -282,11 +304,13 @@ void CMachineManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
|
||||
while (b) {
|
||||
if (th32.th32OwnerProcessID == Pid) {
|
||||
HANDLE oth = OpenThread(THREAD_ALL_ACCESS, FALSE, th32.th32ThreadID);
|
||||
::ResumeThread(oth);
|
||||
CloseHandle(oth);
|
||||
break;
|
||||
if (oth) {
|
||||
::ResumeThread(oth);
|
||||
CloseHandle(oth);
|
||||
}
|
||||
}
|
||||
::Thread32Next(hThreadSnap, &th32);
|
||||
// 冻结挂起了全部线程,解冻也必须逐个恢复;这里不能 break,否则进程只会恢复一个线程
|
||||
b = ::Thread32Next(hThreadSnap, &th32);
|
||||
}
|
||||
::CloseHandle(hThreadSnap);
|
||||
}
|
||||
@@ -554,7 +578,11 @@ LPBYTE CMachineManager::getProcessList()
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32);
|
||||
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
|
||||
|
||||
if (lpBuffer == NULL) {
|
||||
CloseHandle(hSnapshot);
|
||||
DebugPrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return NULL;
|
||||
}
|
||||
lpBuffer[0] = TOKEN_MACHINE_PROCESS;
|
||||
dwOffset = 1;
|
||||
|
||||
@@ -572,6 +600,8 @@ LPBYTE CMachineManager::getProcessList()
|
||||
if ((pe32.th32ProcessID != 0) && (pe32.th32ProcessID != 4) && (pe32.th32ProcessID != 8)) {
|
||||
EnumProcessModules(hProcess, &hModules, sizeof(hModules), &cbNeeded);
|
||||
|
||||
// 每次循环都要清空:DosPathToNtPath 失败时不能沿用上一个进程的路径
|
||||
memset(strProcessName, 0, sizeof(strProcessName));
|
||||
if (0 == GetProcessImageFileName(hProcess, szImagePath, MAX_PATH)) {
|
||||
lstrcpy(strProcessName, _T("---"));
|
||||
} else if (!DosPathToNtPath(szImagePath, strProcessName)) {
|
||||
@@ -590,8 +620,15 @@ LPBYTE CMachineManager::getProcessList()
|
||||
dwLength = sizeof(DWORD) * 5 + sizeof(bool) + MAX_PATH * sizeof(char) + lstrlen(pe32.szExeFile) * sizeof(char) +
|
||||
lstrlen(strProcessName) * sizeof(char) + lstrlen(strProcessUser) * sizeof(char) + 6;
|
||||
// 缓冲区太小,再重新分配下
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL) {
|
||||
// 扩容失败时原缓冲区仍然有效,结束收集并返回已拿到的记录
|
||||
CloseHandle(hProcess);
|
||||
break;
|
||||
}
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, &(pe32.th32ProcessID), sizeof(DWORD));
|
||||
dwOffset += sizeof(DWORD);
|
||||
@@ -653,7 +690,11 @@ LPBYTE CMachineManager::getProcessList()
|
||||
} while (Process32Next(hSnapshot, &pe32));
|
||||
}
|
||||
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
DebugPrivilege(SE_DEBUG_NAME, FALSE);
|
||||
CloseHandle(hSnapshot);
|
||||
@@ -687,6 +728,13 @@ LPBYTE CMachineManager::getWindowsList()
|
||||
{
|
||||
LPBYTE lpBuffer = NULL;
|
||||
EnumWindows((WNDENUMPROC)EnumWindowsProc, (LPARAM)&lpBuffer);
|
||||
if (lpBuffer == NULL) {
|
||||
// 一个可见窗口都没有时回调不会分配缓冲区:仍要回一个只带令牌的包,
|
||||
// 否则下面写 lpBuffer[0] 就是空指针写,客户端会崩溃。
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1);
|
||||
if (lpBuffer == NULL)
|
||||
return NULL;
|
||||
}
|
||||
lpBuffer[0] = TOKEN_MACHINE_WINDOWS;
|
||||
return lpBuffer;
|
||||
}
|
||||
@@ -739,13 +787,18 @@ bool CALLBACK CMachineManager::EnumWindowsProc(HWND hwnd, LPARAM lParam)
|
||||
::GetWindowText(hwnd, m_ibfo.strTitle, 1024);
|
||||
|
||||
if (lpBuffer == NULL) {
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1);
|
||||
dwOffset = 1;
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, dwOffset + sizeof(WINDOWSINFO));
|
||||
if (lpBuffer == NULL)
|
||||
return true;
|
||||
} else {
|
||||
dwOffset = (DWORD)LocalSize(lpBuffer);
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset + sizeof(WINDOWSINFO), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
// 分配失败时原缓冲区仍然有效,不能把指针覆盖成 NULL
|
||||
if (lpNew == NULL)
|
||||
return true;
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset + sizeof(WINDOWSINFO), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
} catch (...) {
|
||||
return true;
|
||||
}
|
||||
@@ -853,8 +906,15 @@ LPBYTE CMachineManager::getSoftWareList()
|
||||
// 缓冲区太小,再重新分配下
|
||||
dwLength = lstrlen(regDisplayName) * sizeof(char) + lstrlen(regPublisher) * sizeof(char) + lstrlen(regDisplayVersion) *
|
||||
sizeof(char) + lstrlen(regInstallData) * sizeof(char) + lstrlen(regUninstallString) * sizeof(char) + 11;
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL) {
|
||||
// 扩容失败时原缓冲区仍然有效,跳过这一条并结束枚举
|
||||
RegCloseKey(hSubKey);
|
||||
break;
|
||||
}
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, regDisplayName, lstrlen(regDisplayName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(regDisplayName) * sizeof(char) + 2;
|
||||
@@ -870,11 +930,13 @@ LPBYTE CMachineManager::getSoftWareList()
|
||||
|
||||
memcpy(lpBuffer + dwOffset, regUninstallString, lstrlen(regUninstallString) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(regUninstallString) * sizeof(char) + 2;
|
||||
RegCloseKey(hSubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return FALSE; //打开键失败
|
||||
LocalFree(lpBuffer);
|
||||
return NULL; //打开键失败
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
@@ -898,6 +960,7 @@ LPBYTE CMachineManager::getSoftWareList()
|
||||
// 取ParentKeyName键值,判断是否是补丁信息, 是补丁信息键值为"OperatingSystem"
|
||||
RegQueryValueEx(hSubKey, _T("ParentKeyName"), 0, &dwType, (LPBYTE)regBufferValue, &dwRegSize);
|
||||
if (lstrlen(regDisplayName) == 0 || lstrcmp(regBufferValue, _T("OperatingSystem")) == 0) { //判断是否是补丁信息
|
||||
RegCloseKey(hSubKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -935,8 +998,15 @@ LPBYTE CMachineManager::getSoftWareList()
|
||||
// 缓冲区太小,再重新分配下
|
||||
dwLength = lstrlen(regDisplayName) * sizeof(char) + lstrlen(regPublisher) * sizeof(char) + lstrlen(regDisplayVersion) *
|
||||
sizeof(char) + lstrlen(regInstallData) * sizeof(char) + lstrlen(regUninstallString) * sizeof(char) + 11;
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL) {
|
||||
// 扩容失败时原缓冲区仍然有效,跳过这一条并结束枚举
|
||||
RegCloseKey(hSubKey);
|
||||
break;
|
||||
}
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, regDisplayName, lstrlen(regDisplayName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(regDisplayName) * sizeof(char) + 2;
|
||||
@@ -952,15 +1022,23 @@ LPBYTE CMachineManager::getSoftWareList()
|
||||
|
||||
memcpy(lpBuffer + dwOffset, regUninstallString, lstrlen(regUninstallString) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(regUninstallString) * sizeof(char) + 2;
|
||||
RegCloseKey(hSubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else
|
||||
return FALSE; //打开键失败
|
||||
} else {
|
||||
//打开键失败
|
||||
LocalFree(lpBuffer);
|
||||
return NULL;
|
||||
}
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
return lpBuffer;
|
||||
}
|
||||
@@ -991,6 +1069,12 @@ LPBYTE CMachineManager::getIEHistoryList()
|
||||
DWORD dwOffset = 1;
|
||||
DWORD dwLength = 0;
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 10000);
|
||||
if (lpBuffer == NULL) {
|
||||
if (SUCCEEDED(hr) && puhs != NULL)
|
||||
puhs->Release();
|
||||
CoUninitialize();
|
||||
return NULL;
|
||||
}
|
||||
lpBuffer[0] = TOKEN_MACHINE_HTML;
|
||||
Browsinghistory m_Browsinghistory;
|
||||
dwLength = sizeof(Browsinghistory);
|
||||
@@ -1012,12 +1096,20 @@ LPBYTE CMachineManager::getIEHistoryList()
|
||||
SYSTEMTIME st;
|
||||
if (&su.ftLastVisited != NULL) {
|
||||
FileTimeToSystemTime(&su.ftLastVisited, &st);
|
||||
wsprintf(m_Browsinghistory.strTime, _T("%d-%d-%d %d:%d:%d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wSecond, st.wMinute);
|
||||
wsprintf(m_Browsinghistory.strTime, _T("%d-%02d-%02d %02d:%02d:%02d"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
} else {
|
||||
wsprintf(m_Browsinghistory.strTime, _T("---"));
|
||||
}
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL) {
|
||||
// 扩容失败时原缓冲区仍然有效,停止收集,返回已拿到的记录
|
||||
CoTaskMemFree(su.pwcsUrl);
|
||||
CoTaskMemFree(su.pwcsTitle);
|
||||
break;
|
||||
}
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, &m_Browsinghistory, sizeof(Browsinghistory));
|
||||
dwOffset += sizeof(Browsinghistory);
|
||||
@@ -1028,11 +1120,15 @@ LPBYTE CMachineManager::getIEHistoryList()
|
||||
su.pwcsTitle = nullptr;
|
||||
}
|
||||
pesu->Release();
|
||||
puhs->Release();
|
||||
}
|
||||
puhs->Release();
|
||||
}
|
||||
CoUninitialize();
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
return lpBuffer;
|
||||
}
|
||||
|
||||
@@ -1053,15 +1149,22 @@ void CMachineManager::FindFavoritesUrl(char* searchfilename, PBYTE &lpFUBuffer,
|
||||
char tmpPath[MAX_PATH] = { 0 };
|
||||
DWORD dwFULength = 0;
|
||||
|
||||
lstrcat(favpath, searchfilename);
|
||||
// 拼接要限长:收藏夹目录较深时 searchfilename 可能已接近 MAX_PATH
|
||||
if (lstrlen(searchfilename) + 4 >= MAX_PATH)
|
||||
return;
|
||||
lstrcpyn(favpath, searchfilename, MAX_PATH - 4);
|
||||
lstrcat(favpath, _T("\\*.*"));
|
||||
|
||||
WIN32_FIND_DATA fd;
|
||||
ZeroMemory(&fd, sizeof(WIN32_FIND_DATA));
|
||||
|
||||
HANDLE hFind = FindFirstFile(favpath, &fd); // 文件后缀都是 url
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
do {
|
||||
if (fd.cFileName[0] != _T('.')) {
|
||||
if (lstrlen(searchfilename) + 1 + lstrlen(fd.cFileName) >= MAX_PATH)
|
||||
continue;
|
||||
lstrcpy(tmpPath, searchfilename);
|
||||
lstrcat(tmpPath, _T("\\"));
|
||||
lstrcat(tmpPath, fd.cFileName);
|
||||
@@ -1074,8 +1177,13 @@ void CMachineManager::FindFavoritesUrl(char* searchfilename, PBYTE &lpFUBuffer,
|
||||
|
||||
dwFULength = lstrlen(buf) * sizeof(char) + lstrlen(fd.cFileName) * sizeof(char) + 4;
|
||||
|
||||
if (LocalSize(lpFUBuffer) < (dwFUOffset + dwFULength))
|
||||
lpFUBuffer = (LPBYTE)LocalReAlloc(lpFUBuffer, (dwFUOffset + dwFULength) + 1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpFUBuffer) < (dwFUOffset + dwFULength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpFUBuffer, (dwFUOffset + dwFULength) + 1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
// 扩容失败时原缓冲区仍然有效:跳过这条记录,不能让指针变成 NULL
|
||||
if (lpNew == NULL)
|
||||
continue;
|
||||
lpFUBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpFUBuffer + dwFUOffset, fd.cFileName, lstrlen(fd.cFileName) * sizeof(char) + 2);
|
||||
dwFUOffset += lstrlen(fd.cFileName) * sizeof(char) + 2;
|
||||
@@ -1094,21 +1202,32 @@ LPBYTE CMachineManager::getFavoritesUrlList()
|
||||
char favpath[MAX_PATH] = { 0 };
|
||||
|
||||
// 从注册表获取收藏夹所在位置
|
||||
HKEY hKEY;
|
||||
HKEY hKEY = NULL;
|
||||
DWORD type = REG_SZ;
|
||||
LPCTSTR path = _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders");
|
||||
DWORD cbData = 80;
|
||||
::RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKEY);
|
||||
::RegQueryValueEx(hKEY, _T("Favorites"), NULL, &type, (LPBYTE)favpath, &cbData);
|
||||
::RegCloseKey(hKEY);
|
||||
DWORD cbData = sizeof(favpath);
|
||||
// RegOpenKeyEx 失败时 hKEY 是无效值,不能再拿它查值/关闭
|
||||
if (::RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKEY) == ERROR_SUCCESS) {
|
||||
if (::RegQueryValueEx(hKEY, _T("Favorites"), NULL, &type, (LPBYTE)favpath, &cbData) != ERROR_SUCCESS)
|
||||
favpath[0] = 0;
|
||||
::RegCloseKey(hKEY);
|
||||
}
|
||||
favpath[MAX_PATH - 1] = 0;
|
||||
|
||||
LPBYTE lpFUBuffer = (LPBYTE)LocalAlloc(LPTR, 10000);
|
||||
if (lpFUBuffer == NULL)
|
||||
return NULL;
|
||||
lpFUBuffer[0] = TOKEN_MACHINE_FAVORITES;
|
||||
|
||||
int dwFUOffset = 1;
|
||||
FindFavoritesUrl(favpath, lpFUBuffer, dwFUOffset);
|
||||
if (favpath[0] != 0)
|
||||
FindFavoritesUrl(favpath, lpFUBuffer, dwFUOffset);
|
||||
|
||||
lpFUBuffer = (LPBYTE)LocalReAlloc(lpFUBuffer, dwFUOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpFUBuffer, dwFUOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpFUBuffer = lpNew;
|
||||
}
|
||||
|
||||
return lpFUBuffer;
|
||||
}
|
||||
@@ -1141,15 +1260,30 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
|
||||
DWORD dwSize = 1024 * 8;
|
||||
|
||||
sc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
|
||||
if (sc == NULL) {
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//第一次调用来得到需要多大的内存区
|
||||
EnumServicesStatus(sc, dwScType, SERVICE_STATE_ALL, lpServices, size, &size, &ret, NULL);
|
||||
//申请需要的内存
|
||||
lpServices = (LPENUM_SERVICE_STATUS)LocalAlloc(LPTR, size);
|
||||
if (lpServices == NULL) {
|
||||
CloseServiceHandle(sc);
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return NULL;
|
||||
}
|
||||
EnumServicesStatus(sc, dwScType, SERVICE_STATE_ALL, lpServices, size, &size, &ret, NULL);
|
||||
|
||||
// 申请Buffer内存
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024 * sizeof(char));
|
||||
if (lpBuffer == NULL) {
|
||||
LocalFree(lpServices);
|
||||
CloseServiceHandle(sc);
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return NULL;
|
||||
}
|
||||
lpBuffer[0] = TOKEN_MACHINE_SERVICE_LIST;
|
||||
dwOffset = 1;
|
||||
|
||||
@@ -1166,7 +1300,7 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
|
||||
// 描述
|
||||
memset(lpDescription, 0, sizeof(lpDescription));
|
||||
if (((LPSERVICE_DESCRIPTION)szInfo)->lpDescription != NULL)
|
||||
lstrcpy(lpDescription, ((LPSERVICE_DESCRIPTION)szInfo)->lpDescription);
|
||||
lstrcpyn(lpDescription, ((LPSERVICE_DESCRIPTION)szInfo)->lpDescription, sizeof(lpDescription));
|
||||
|
||||
// 状态
|
||||
memset(strState, 0, sizeof(strState));
|
||||
@@ -1200,6 +1334,7 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
|
||||
ZeroMemory(szInfo, dwSize);
|
||||
dwSize = 1024 * 8;
|
||||
if (!QueryServiceConfig(sh, (LPQUERY_SERVICE_CONFIG)szInfo, dwSize, &dwSize)) {
|
||||
CloseServiceHandle(sh);
|
||||
continue;
|
||||
}
|
||||
// 启动类型
|
||||
@@ -1234,8 +1369,15 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
|
||||
|
||||
//==========================================================================================
|
||||
// 缓冲区太小,再重新分配下
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
// 扩容失败时原缓冲区仍然有效,保留已收集的记录并结束枚举
|
||||
if (lpNew == NULL) {
|
||||
CloseServiceHandle(sh);
|
||||
break;
|
||||
}
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
// 显示名称
|
||||
memcpy(lpBuffer + dwOffset, lpServices[i].lpDisplayName, lstrlen(lpServices[i].lpDisplayName) * sizeof(char) + sizeof(char));
|
||||
@@ -1277,8 +1419,14 @@ LPBYTE CMachineManager::getServiceList(DWORD dwScType)
|
||||
}
|
||||
|
||||
CloseServiceHandle(sc);
|
||||
LocalFree(lpServices);
|
||||
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
// 收缩到实际长度;失败时原缓冲区仍有效,直接按原大小返回
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return lpBuffer;
|
||||
}
|
||||
@@ -1303,7 +1451,16 @@ void CMachineManager::SendHostsList()
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
dwSize = GetFileSize(hFile, NULL);
|
||||
// GetFileSize 失败返回 INVALID_FILE_SIZE:不加判断会按 0xFFFFFFFF 分配并读入
|
||||
if (dwSize == INVALID_FILE_SIZE) {
|
||||
CloseHandle(hFile);
|
||||
return;
|
||||
}
|
||||
lpBuffer = (LPBYTE)LocalAlloc(LPTR, dwSize + 2);
|
||||
if (lpBuffer == NULL) {
|
||||
CloseHandle(hFile);
|
||||
return;
|
||||
}
|
||||
if (!ReadFile(hFile, lpBuffer + 1, dwSize, &dwRead, NULL)) {
|
||||
LocalFree(lpBuffer);
|
||||
CloseHandle(hFile);
|
||||
@@ -1320,46 +1477,63 @@ void CMachineManager::SendHostsList()
|
||||
LPBYTE CMachineManager::getNetStateList()
|
||||
{
|
||||
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, 1024);
|
||||
if (lpBuffer == NULL)
|
||||
return NULL;
|
||||
lpBuffer[0] = TOKEN_MACHINE_NETSTATE;
|
||||
DWORD dwOffset = 1;
|
||||
DWORD dwLength = 0;
|
||||
WCHAR* temp;
|
||||
// 定义扩展函数指针
|
||||
PFNAllocateAndGetTcpExTableFromStack pAllocateAndGetTcpExTableFromStack;
|
||||
PFNAllocateAndGetUdpExTableFromStack pAllocateAndGetUdpExTableFromStack;
|
||||
|
||||
// 获取扩展函数的入口地址
|
||||
HMODULE hModule = ::LoadLibrary(_T("iphlpapi.dll"));
|
||||
pAllocateAndGetTcpExTableFromStack =
|
||||
pAllocateAndGetTcpExTableFromStack = hModule == NULL ? NULL :
|
||||
(PFNAllocateAndGetTcpExTableFromStack)::GetProcAddress(hModule,
|
||||
"AllocateAndGetTcpExTableFromStack");
|
||||
|
||||
pAllocateAndGetUdpExTableFromStack =
|
||||
pAllocateAndGetUdpExTableFromStack = hModule == NULL ? NULL :
|
||||
(PFNAllocateAndGetUdpExTableFromStack)::GetProcAddress(hModule,
|
||||
"AllocateAndGetUdpExTableFromStack");
|
||||
|
||||
if (pAllocateAndGetTcpExTableFromStack != NULL || pAllocateAndGetUdpExTableFromStack != NULL) {
|
||||
// 两个导出函数要么都有(XP/2003)、要么都没有(Vista+):用 && 避免只匹配到一个时空指针调用
|
||||
if (pAllocateAndGetTcpExTableFromStack != NULL && pAllocateAndGetUdpExTableFromStack != NULL) {
|
||||
// 调用扩展函数,获取TCP扩展连接表和UDP扩展监听表
|
||||
PMIB_TCPEXTABLE pTcpExTable;
|
||||
PMIB_UDPEXTABLE pUdpExTable;
|
||||
PMIB_TCPEXTABLE pTcpExTable = NULL;
|
||||
PMIB_UDPEXTABLE pUdpExTable = NULL;
|
||||
|
||||
// pTcpExTable和pUdpExTable所指的缓冲区自动由扩展函数在进程堆中申请
|
||||
if (pAllocateAndGetTcpExTableFromStack(&pTcpExTable, TRUE, GetProcessHeap(), 2, 2) != 0) {
|
||||
if (pTcpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return NULL;
|
||||
}
|
||||
if (pAllocateAndGetUdpExTableFromStack(&pUdpExTable, TRUE, GetProcessHeap(), 2, 2) != 0) {
|
||||
if (pTcpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
|
||||
if (pUdpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 给系统内的所有进程拍一个快照
|
||||
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hProcessSnap == INVALID_HANDLE_VALUE) {
|
||||
if (pTcpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
|
||||
if (pUdpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char szLocalAddr[128];
|
||||
char szRemoteAddr[128];
|
||||
char szProcessName[128];
|
||||
in_addr inadLocal, inadRemote;
|
||||
char strState[128];
|
||||
DWORD dwRemotePort = 0;
|
||||
@@ -1419,24 +1593,26 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
// 远程IP地址
|
||||
inadRemote.s_addr = pTcpExTable->table[i].dwRemoteAddr;
|
||||
|
||||
temp = char2wchar_t(inet_ntoa(inadLocal));
|
||||
wsprintf(szLocalAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & pTcpExTable->table[i].dwLocalPort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
temp = char2wchar_t(inet_ntoa(inadRemote));
|
||||
wsprintf(szRemoteAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
// 客户端是 MultiByte 工程,缓冲区是 char[]:这里必须用窄字符格式化,
|
||||
// 不能先转成 WCHAR* 再交给 _T("%s")(ANSI 的 %s 只会读到首字符,地址显示成 0/1)。
|
||||
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
|
||||
ntohs((unsigned short)(0x0000FFFF & pTcpExTable->table[i].dwLocalPort)));
|
||||
sprintf_s(szRemoteAddr, "%s:%u", inet_ntoa(inadRemote),
|
||||
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
|
||||
// 打印出此入口的信息
|
||||
char strProcessName[100] = { 0 };
|
||||
const char* strType = _T("[TCP]");
|
||||
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pTcpExTable->table[i].dwProcessId, szProcessName));
|
||||
ProcessPidToName(hProcessSnap, pTcpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
|
||||
|
||||
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
|
||||
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(strState) *
|
||||
sizeof(char) + 12;
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL)
|
||||
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
|
||||
@@ -1461,23 +1637,25 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
for (DWORD i = 0; i < pUdpExTable->dwNumEntries; ++i) {
|
||||
// 本地IP地址
|
||||
inadLocal.s_addr = pUdpExTable->table[i].dwLocalAddr;
|
||||
temp = char2wchar_t(inet_ntoa(inadLocal));
|
||||
wsprintf(szLocalAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
|
||||
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
|
||||
// 打印出此入口的信息
|
||||
char strProcessName[100] = { 0 };
|
||||
const char* strType = _T("[UDP]");
|
||||
const char* szRemoteAddr = _T("*.*.*.*:*");
|
||||
const char* szUDPState = _T(" ");
|
||||
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, szProcessName));
|
||||
ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
|
||||
|
||||
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
|
||||
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(szUDPState) *
|
||||
sizeof(char) + 12;
|
||||
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL)
|
||||
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
|
||||
@@ -1498,28 +1676,43 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
dwOffset += lstrlen(szUDPState) * sizeof(char) + 2;
|
||||
}
|
||||
::CloseHandle(hProcessSnap);
|
||||
::LocalFree(pTcpExTable);
|
||||
::LocalFree(pUdpExTable);
|
||||
// 扩展表是扩展函数在 GetProcessHeap() 上用 HeapAlloc 申请的,应对应用 HeapFree 释放
|
||||
if (pTcpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpExTable);
|
||||
if (pUdpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
|
||||
::FreeLibrary(hModule);
|
||||
} else {
|
||||
char szLocalAddr[128];
|
||||
char szRemoteAddr[128];
|
||||
char szProcessName[128];
|
||||
in_addr inadLocal, inadRemote;
|
||||
char strState[128];
|
||||
DWORD dwRemotePort = 0;
|
||||
|
||||
PMIB_TCPEXTABLE_VISTA pTcpTable_Vista;
|
||||
_InternalGetTcpTable2 pGetTcpTable = (_InternalGetTcpTable2)GetProcAddress(hModule, "InternalGetTcpTable2");
|
||||
if (pGetTcpTable == NULL)
|
||||
PMIB_TCPEXTABLE_VISTA pTcpTable_Vista = NULL;
|
||||
_InternalGetTcpTable2 pGetTcpTable = hModule == NULL ? NULL :
|
||||
(_InternalGetTcpTable2)GetProcAddress(hModule, "InternalGetTcpTable2");
|
||||
if (pGetTcpTable == NULL) {
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pGetTcpTable(&pTcpTable_Vista, GetProcessHeap(), 1))
|
||||
if (pGetTcpTable(&pTcpTable_Vista, GetProcessHeap(), 1)) {
|
||||
if (pTcpTable_Vista)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 给系统内的所有进程拍一个快照
|
||||
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hProcessSnap == INVALID_HANDLE_VALUE) {
|
||||
if (pTcpTable_Vista)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
|
||||
::FreeLibrary(hModule);
|
||||
LocalFree(lpBuffer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1577,25 +1770,25 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
// 远程IP地址
|
||||
inadRemote.s_addr = pTcpTable_Vista->table[i].dwRemoteAddr;
|
||||
|
||||
temp = char2wchar_t(inet_ntoa(inadLocal));
|
||||
wsprintf(szLocalAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & pTcpTable_Vista->table[i].dwLocalPort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
temp = char2wchar_t(inet_ntoa(inadRemote));
|
||||
wsprintf(szRemoteAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
|
||||
ntohs((unsigned short)(0x0000FFFF & pTcpTable_Vista->table[i].dwLocalPort)));
|
||||
sprintf_s(szRemoteAddr, "%s:%u", inet_ntoa(inadRemote),
|
||||
ntohs((unsigned short)(0x0000FFFF & dwRemotePort)));
|
||||
|
||||
// 打印出此入口的信息
|
||||
char strProcessName[100] = { 0 };
|
||||
const char* strType = _T("[TCP]");
|
||||
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pTcpTable_Vista->table[i].dwProcessId, szProcessName));
|
||||
ProcessPidToName(hProcessSnap, pTcpTable_Vista->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
|
||||
|
||||
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
|
||||
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(strState) *
|
||||
sizeof(char) + 12;
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL)
|
||||
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
|
||||
@@ -1619,7 +1812,7 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
PMIB_UDPEXTABLE pUdpExTable = NULL;
|
||||
// 表明为 Vista 或者 7 操作系统
|
||||
PFNInternalGetUdpTableWithOwnerPid pInternalGetUdpTableWithOwnerPid;
|
||||
pInternalGetUdpTableWithOwnerPid =
|
||||
pInternalGetUdpTableWithOwnerPid = hModule == NULL ? NULL :
|
||||
(PFNInternalGetUdpTableWithOwnerPid)GetProcAddress(hModule, "InternalGetUdpTableWithOwnerPid");
|
||||
if (pInternalGetUdpTableWithOwnerPid != NULL) {
|
||||
if (pInternalGetUdpTableWithOwnerPid(&pUdpExTable, GetProcessHeap(), 1)) {
|
||||
@@ -1627,8 +1820,12 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
HeapFree(GetProcessHeap(), 0, pUdpExTable);
|
||||
}
|
||||
|
||||
::CloseHandle(hProcessSnap);
|
||||
if (pTcpTable_Vista)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
|
||||
FreeLibrary(hModule);
|
||||
hModule = NULL;
|
||||
LocalFree(lpBuffer);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1637,23 +1834,25 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
for (DWORD i = 0; i < pUdpExTable->dwNumEntries; ++i) {
|
||||
// 本地IP地址
|
||||
inadLocal.s_addr = pUdpExTable->table[i].dwLocalAddr;
|
||||
temp = char2wchar_t(inet_ntoa(inadLocal));
|
||||
wsprintf(szLocalAddr, _T("%s:%u"), temp,
|
||||
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
|
||||
SAFE_DELETE_AR(temp);
|
||||
sprintf_s(szLocalAddr, "%s:%u", inet_ntoa(inadLocal),
|
||||
ntohs((unsigned short)(0x0000FFFF & pUdpExTable->table[i].dwLocalPort)));
|
||||
|
||||
// 打印出此入口的信息
|
||||
char strProcessName[100] = { 0 };
|
||||
const char* strType = _T("[UDP]");
|
||||
const char* szRemoteAddr = _T("*.*.*.*:*");
|
||||
const char* szUDPState = _T(" ");
|
||||
lstrcpy(strProcessName, ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, szProcessName));
|
||||
ProcessPidToName(hProcessSnap, pUdpExTable->table[i].dwProcessId, strProcessName, sizeof(strProcessName));
|
||||
|
||||
dwLength = lstrlen(strProcessName) * sizeof(char) + sizeof(DWORD) + lstrlen(strType) * sizeof(char) +
|
||||
lstrlen(szLocalAddr) * sizeof(char) + lstrlen(szRemoteAddr) * sizeof(char) + lstrlen(szUDPState) *
|
||||
sizeof(char) + 12;
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength))
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (LocalSize(lpBuffer) < (dwOffset + dwLength)) {
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, (dwOffset + dwLength), LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew == NULL)
|
||||
break; // 扩容失败时保留原缓冲区,已收集的记录照常返回
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
memcpy(lpBuffer + dwOffset, strProcessName, lstrlen(strProcessName) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(strProcessName) * sizeof(char) + 2;
|
||||
@@ -1673,26 +1872,42 @@ LPBYTE CMachineManager::getNetStateList()
|
||||
memcpy(lpBuffer + dwOffset, szUDPState, lstrlen(szUDPState) * sizeof(char) + 2);
|
||||
dwOffset += lstrlen(szUDPState) * sizeof(char) + 2;
|
||||
}
|
||||
|
||||
if (pUdpExTable)
|
||||
::HeapFree(GetProcessHeap(), 0, pUdpExTable);
|
||||
}
|
||||
|
||||
// Vista 分支的资源必须在这里释放:每次刷新网络列表都会走到这里
|
||||
::CloseHandle(hProcessSnap);
|
||||
if (pTcpTable_Vista)
|
||||
::HeapFree(GetProcessHeap(), 0, pTcpTable_Vista);
|
||||
::FreeLibrary(hModule);
|
||||
}
|
||||
|
||||
lpBuffer = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
{
|
||||
LPBYTE lpNew = (LPBYTE)LocalReAlloc(lpBuffer, dwOffset, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpBuffer = lpNew;
|
||||
}
|
||||
|
||||
return lpBuffer;
|
||||
}
|
||||
|
||||
// 将进程ID号(PID)转化为进程名称
|
||||
char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName)
|
||||
char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName, int nSize)
|
||||
{
|
||||
PROCESSENTRY32 processEntry;
|
||||
processEntry.dwSize = sizeof(processEntry);
|
||||
if (ProcessName == NULL || nSize <= 0)
|
||||
return ProcessName;
|
||||
// 找不到的话,默认进程名为“???”
|
||||
lstrcpy(ProcessName, _T("???"));
|
||||
lstrcpyn(ProcessName, _T("???"), nSize);
|
||||
if (!::Process32First(hProcessSnap, &processEntry))
|
||||
return ProcessName;
|
||||
do {
|
||||
if (processEntry.th32ProcessID == ProcessId) { // 就是这个进程
|
||||
lstrcpy(ProcessName, processEntry.szExeFile);
|
||||
// szExeFile 最长 MAX_PATH,目标缓冲区可能只有 100 字节,必须限长拷贝
|
||||
lstrcpyn(ProcessName, processEntry.szExeFile, nSize);
|
||||
break;
|
||||
}
|
||||
} while (::Process32Next(hProcessSnap, &processEntry));
|
||||
@@ -1700,16 +1915,6 @@ char* CMachineManager::ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, ch
|
||||
}
|
||||
|
||||
|
||||
wchar_t* CMachineManager::char2wchar_t(char* cstr)
|
||||
{
|
||||
int len = MultiByteToWideChar(CP_ACP, 0, cstr, -1, NULL, 0);
|
||||
wchar_t* wstr = new wchar_t[len];
|
||||
memset(wstr, 0, len * sizeof(wchar_t));
|
||||
MultiByteToWideChar(CP_ACP, 0, cstr, -1, wstr, len);
|
||||
return wstr;
|
||||
}
|
||||
|
||||
|
||||
BOOL CMachineManager::EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable)
|
||||
{
|
||||
HANDLE hToken;
|
||||
@@ -1733,18 +1938,21 @@ BOOL CMachineManager::EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable)
|
||||
void CMachineManager::DeleteService(LPBYTE lpBuffer, UINT nSize)
|
||||
{
|
||||
EnablePrivilege(SE_DEBUG_NAME, TRUE);
|
||||
SERVICE_STATUS status;
|
||||
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
|
||||
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS);
|
||||
|
||||
DWORD _err = GetLastError();
|
||||
BOOL isSuccess = QueryServiceStatus(service, &status);
|
||||
if (status.dwCurrentState != SERVICE_STOPPED) {
|
||||
isSuccess = ControlService(service, SERVICE_CONTROL_STOP, &status);
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
|
||||
if (scm == NULL) {
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return;
|
||||
}
|
||||
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS);
|
||||
if (service != NULL) {
|
||||
SERVICE_STATUS status = { 0 };
|
||||
if (QueryServiceStatus(service, &status) && status.dwCurrentState != SERVICE_STOPPED) {
|
||||
ControlService(service, SERVICE_CONTROL_STOP, &status);
|
||||
}
|
||||
::DeleteService(service);
|
||||
CloseServiceHandle(service);
|
||||
}
|
||||
isSuccess = ::DeleteService(service);
|
||||
CloseServiceHandle(service);
|
||||
CloseServiceHandle(scm);
|
||||
|
||||
// 稍稍Sleep下,防止出错
|
||||
@@ -1757,25 +1965,55 @@ void CMachineManager::DeleteService(LPBYTE lpBuffer, UINT nSize)
|
||||
void CMachineManager::MyControlService(LPBYTE lpBuffer, UINT nType)
|
||||
{
|
||||
EnablePrivilege(SE_DEBUG_NAME, TRUE);
|
||||
SERVICE_STATUS status;
|
||||
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); // 打开服务管理对象
|
||||
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); // 打开服务管理对象
|
||||
if (scm == NULL) {
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS); // 打开www服务
|
||||
SC_HANDLE service = OpenService(scm, (char*)lpBuffer, SERVICE_ALL_ACCESS); // 打开服务
|
||||
if (service == NULL) {
|
||||
CloseServiceHandle(scm);
|
||||
SendServicesList(m_dwServiceType);
|
||||
EnablePrivilege(SE_DEBUG_NAME, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
SERVICE_STATUS status = { 0 };
|
||||
BOOL isSuccess = QueryServiceStatus(service, &status);
|
||||
|
||||
switch (nType) {
|
||||
case 0:
|
||||
case 0: {
|
||||
if (status.dwCurrentState == SERVICE_STOP_PENDING) {
|
||||
// 服务端“重启服务”是 STOP 后 100ms 就发 START:这里等服务真正停下来,
|
||||
// 否则 StartService 会以服务仍在停止中失败,表现为“重启后服务没起来”。
|
||||
for (int i = 0; i < 50 && status.dwCurrentState == SERVICE_STOP_PENDING; i++) {
|
||||
Sleep(100);
|
||||
if (!QueryServiceStatus(service, &status))
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (status.dwCurrentState == SERVICE_STOPPED) { //服务停止状态 就启动服务
|
||||
isSuccess = StartService(service, NULL, NULL);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
}
|
||||
case 1: {
|
||||
if (status.dwCurrentState != SERVICE_STOPPED) {
|
||||
isSuccess = ControlService(service, SERVICE_CONTROL_STOP, &status);
|
||||
if (status.dwCurrentState != SERVICE_STOP_PENDING)
|
||||
isSuccess = ControlService(service, SERVICE_CONTROL_STOP, &status);
|
||||
// 同步等待停止完成(最多 5 秒),让紧随其后的 START 能成功
|
||||
if (isSuccess || status.dwCurrentState == SERVICE_STOP_PENDING) {
|
||||
for (int i = 0; i < 50 && status.dwCurrentState != SERVICE_STOPPED; i++) {
|
||||
Sleep(100);
|
||||
if (!QueryServiceStatus(service, &status))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
if (status.dwCurrentState == SERVICE_RUNNING) {
|
||||
isSuccess = ControlService(service, SERVICE_CONTROL_PAUSE, &status);
|
||||
@@ -1838,7 +2076,10 @@ PBYTE CMachineManager::GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset
|
||||
|
||||
pRegisteredTask->get_Definition(&pDefinition);
|
||||
if (pDefinition) {
|
||||
// GetProgramPath 内部会 Release 传入的 ITaskDefinition(接管所有权),
|
||||
// 这里不能再释放一次,否则是双重 Release,枚举任务时客户端直接崩溃。
|
||||
GetProgramPath(pDefinition, &exepath);
|
||||
pDefinition = NULL;
|
||||
}
|
||||
|
||||
switch (taskState) {
|
||||
@@ -1891,6 +2132,7 @@ PBYTE CMachineManager::GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offs
|
||||
//获取文件夹个数
|
||||
hr = pFolders->get_Count(&Count);
|
||||
if (FAILED(hr)) {
|
||||
pFolders->Release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1910,6 +2152,7 @@ PBYTE CMachineManager::GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offs
|
||||
continue;
|
||||
}
|
||||
GetFolderAll(ispFolder, data, offset);
|
||||
ispFolder->Release();
|
||||
}
|
||||
|
||||
pFolders->Release();
|
||||
@@ -1969,7 +2212,27 @@ PBYTE CMachineManager::GetRoot()
|
||||
lpList[0] = TOKEN_MACHINE_TASKLIST;
|
||||
int offset = 1;
|
||||
GetFolderAll(pRootFolder, lpList, offset);
|
||||
Send((LPBYTE)lpList, (UINT)LocalSize(lpList));
|
||||
|
||||
// 只发实际数据,不要把 32KB 预分配缓冲区整个发出去。
|
||||
// 服务端解析靠每条记录末尾的两个 NUL 判断结束,所以非空列表要多带 2 字节结束标记;
|
||||
// 没有任务时只发令牌 1 字节(长度为 1 时服务端循环直接结束)。
|
||||
UINT dwSend = 1;
|
||||
if (offset > 1) {
|
||||
dwSend = (UINT)(offset + 2);
|
||||
if ((UINT)LocalSize(lpList) < dwSend) {
|
||||
PBYTE lpNew = (PBYTE)LocalReAlloc(lpList, dwSend, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (lpNew != NULL)
|
||||
lpList = lpNew;
|
||||
}
|
||||
if ((UINT)LocalSize(lpList) >= dwSend) {
|
||||
lpList[offset] = 0;
|
||||
lpList[offset + 1] = 0;
|
||||
} else {
|
||||
// 扩不出结束标记时只回令牌,避免服务端越界读
|
||||
dwSend = 1;
|
||||
}
|
||||
}
|
||||
Send((LPBYTE)lpList, dwSend);
|
||||
LocalFree(lpList);
|
||||
}
|
||||
pRootFolder->Release();
|
||||
@@ -2033,15 +2296,18 @@ BOOL CMachineManager::CreateTask(LPBYTE lpBuffer)
|
||||
//设置被结束后自动延迟10分钟运行
|
||||
pSettings->put_StartWhenAvailable(VARIANT_TRUE);
|
||||
//创建触发器
|
||||
ITrigger* pTrigger;
|
||||
ITrigger* pTrigger = NULL;
|
||||
hr = pTriggers->Create(TASK_TRIGGER_LOGON, &pTrigger);
|
||||
|
||||
IAction* pAction;
|
||||
IAction* pAction = NULL;
|
||||
//设置执行
|
||||
pActions->Create(TASK_ACTION_EXEC, &pAction);
|
||||
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
|
||||
//设置程序路径等信息
|
||||
pExecAction->put_Path(CharToBSTR(path));
|
||||
if (pAction) {
|
||||
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
|
||||
//设置程序路径等信息
|
||||
if (pExecAction)
|
||||
pExecAction->put_Path(CharToBSTR(path));
|
||||
}
|
||||
//注册
|
||||
hr = pRootFolder->RegisterTaskDefinition(CharToBSTR(taskname), pDefinition, TASK_CREATE_OR_UPDATE,
|
||||
_variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(L""), &pRegisteredTask);
|
||||
@@ -2162,10 +2428,13 @@ void CMachineManager::SaveData(const std::string& taskname, const std::string& p
|
||||
sum = nameLEN + taskpathLEN + statusLEN + exeLEN + sizeof(DATE) * 2;
|
||||
// 动态扩展缓冲区
|
||||
if (LocalSize(lpList) < (offset + sum)) {
|
||||
lpList = (BYTE*)LocalReAlloc(lpList, LocalSize(lpList)+4*1024, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
if (!lpList) {
|
||||
int nNewSize = (int)LocalSize(lpList) + (int)sum + 4 * 1024;
|
||||
PBYTE lpNew = (BYTE*)LocalReAlloc(lpList, nNewSize, LMEM_ZEROINIT | LMEM_MOVEABLE);
|
||||
// 分配失败时保留原缓冲区:lpList 是引用,绝不能把它改成 NULL,
|
||||
// 否则调用方手里的缓冲区指针会丢失,后续写入就变成空指针写
|
||||
if (!lpNew)
|
||||
return;
|
||||
}
|
||||
lpList = lpNew;
|
||||
}
|
||||
|
||||
//拷贝数据
|
||||
|
||||
@@ -34,8 +34,7 @@ private:
|
||||
HWND GetHwndByPid(DWORD dwProcessID);
|
||||
bool Is64BitOS();
|
||||
bool Is64BitPorcess(DWORD dwProcessID);
|
||||
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
|
||||
wchar_t* char2wchar_t(char* cstr);
|
||||
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName, int nSize);
|
||||
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
|
||||
void DeleteService(LPBYTE lpBuffer, UINT nSize);
|
||||
void MyControlService(LPBYTE lpBuffer, UINT nType);
|
||||
|
||||
+17
-1
@@ -14,6 +14,7 @@
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <emmintrin.h> // SSE2
|
||||
#include "common/config.h"
|
||||
#include "VideoEncoderBase.h"
|
||||
@@ -149,6 +150,9 @@ public:
|
||||
int m_FrameID; // 帧序号
|
||||
int m_GOP; // 关键帧间隔
|
||||
bool m_SendKeyFrame; // 发送关键帧
|
||||
// Web 端请求下一帧强制 IDR(COMMAND_SCREEN_KEYFRAME_REQ)。只对 H.264 生效;
|
||||
// 必须在成功编码出 IDR 后清除,否则 ScanNextScreen 失败/硬编 EAGAIN 时请求会丢。
|
||||
std::atomic<bool> m_bForceKeyFrame{false};
|
||||
std::unique_ptr<VideoEncoderBase> m_encoder; // 编码器,ensureEncoder() lazy 创建,走 EncoderFactory 探测
|
||||
bool m_bEncoderPrimed = false; // encoder 是否已成功产出过一个包;
|
||||
// false 时禁止 skip——避免单显示器路径
|
||||
@@ -257,6 +261,11 @@ public:
|
||||
m_BlockBuffers[blockY] = new BYTE[m_ulFullWidth * m_ulFullHeight * 4 * 2 / m_BlockNum + 12];
|
||||
}
|
||||
}
|
||||
|
||||
// 请求下一帧强制 IDR(可跨线程调用)。只影响 H.264 编码路径;
|
||||
// 实际清除发生在成功编码出 IDR 之后。
|
||||
void RequestKeyFrame() { m_bForceKeyFrame.store(true, std::memory_order_relaxed); }
|
||||
|
||||
virtual ~ScreenCapture()
|
||||
{
|
||||
if (m_BitmapInfor_Full != NULL) {
|
||||
@@ -876,7 +885,12 @@ public:
|
||||
{
|
||||
BYTE algo = m_bAlgorithm;
|
||||
int frameID = m_FrameID + 1;
|
||||
bool keyFrame = (frameID % m_GOP == 0);
|
||||
// Web 端可请求强制 IDR(COMMAND_SCREEN_KEYFRAME_REQ),只对 H.264 有意义。
|
||||
// 标志在成功编码出 IDR 后才清除(见下方 H.264 keyframe 分支),
|
||||
// 保证 ScanNextScreen 失败或硬编 EAGAIN 时请求不会丢失。
|
||||
bool forceKeyFrame = (algo == ALGORITHM_H264) &&
|
||||
m_bForceKeyFrame.load(std::memory_order_relaxed);
|
||||
bool keyFrame = forceKeyFrame || (frameID % m_GOP == 0);
|
||||
m_RectBuffer[0] = keyFrame ? TOKEN_KEYFRAME : TOKEN_NEXTSCREEN;
|
||||
LPBYTE data = m_RectBuffer + 1;
|
||||
// 写入使用了哪种算法
|
||||
@@ -967,6 +981,8 @@ public:
|
||||
*ulNextSendLength = 1 + offset + encoded_size;
|
||||
memcpy(data + offset, encoded_data, encoded_size);
|
||||
m_bEncoderPrimed = true; // 与下方 FirstBuffer 同步:自此 skip 安全
|
||||
// IDR 已成功产出,满足(可能的)强制关键帧请求。
|
||||
m_bForceKeyFrame.store(false, std::memory_order_relaxed);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -930,6 +930,13 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
SwitchScreen();
|
||||
break;
|
||||
}
|
||||
case COMMAND_SCREEN_KEYFRAME_REQ: {
|
||||
// Web 屏幕墙恢复/新加入者请求:下一帧强制 IDR,立即重建参考链。
|
||||
if (m_ScreenSpyObject) {
|
||||
m_ScreenSpyObject->RequestKeyFrame();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMD_FULL_SCREEN: {
|
||||
int fullScreen = szBuffer[1];
|
||||
iniFile cfg(CLIENT_PATH);
|
||||
|
||||
+2
-2
@@ -103,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,4,0
|
||||
FILEVERSION 1,0,4,1
|
||||
PRODUCTVERSION 1,0,0,1
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
@@ -121,7 +121,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "FUCK THE UNIVERSE"
|
||||
VALUE "FileDescription", "A GHOST"
|
||||
VALUE "FileVersion", "1.0.4.0"
|
||||
VALUE "FileVersion", "1.0.4.1"
|
||||
VALUE "InternalName", "ServerDll.dll"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2019-2026"
|
||||
VALUE "OriginalFilename", "ServerDll.dll"
|
||||
|
||||
Binary file not shown.
@@ -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();
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
<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" />
|
||||
@@ -299,6 +300,7 @@
|
||||
<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" />
|
||||
|
||||
@@ -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>
|
||||
+22
-4
@@ -371,10 +371,28 @@ BOOL CreateDirectoryRecursively(const char* path)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const char* GetInstallDirectory(const char * startupName)
|
||||
const char* GetInstallDirectory(const char * startupName, int flag)
|
||||
{
|
||||
static char folder[MAX_PATH] = { 0 };
|
||||
if (GetEnvironmentVariableA("ProgramData", folder, MAX_PATH) > 0) {
|
||||
char env[32] = { 0 };
|
||||
switch (flag) {
|
||||
case 1:
|
||||
strcpy(env, "ProgramFiles");
|
||||
break;
|
||||
case 2:
|
||||
strcpy(env, "SystemRoot"); // Windows 目录,通常 C:\Windows
|
||||
break;
|
||||
case 3:
|
||||
strcpy(env, "APPDATA"); // 当前用户的漫游应用数据目录,即 ...\AppData\Roaming
|
||||
break;
|
||||
case 4:
|
||||
strcpy(env, "LOCALAPPDATA"); // 当前用户的本地应用数据目录,即 ...\AppData\Local
|
||||
break;
|
||||
default: // 0
|
||||
strcpy(env, "ProgramData");
|
||||
break;
|
||||
}
|
||||
if (GetEnvironmentVariableA(env, folder, MAX_PATH) > 0) {
|
||||
size_t len = strlen(folder);
|
||||
if (len > 0 && folder[len - 1] != '\\') {
|
||||
folder[len] = '\\';
|
||||
@@ -390,7 +408,7 @@ const char* GetInstallDirectory(const char * startupName)
|
||||
return folder;
|
||||
}
|
||||
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log)
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
return 1;
|
||||
@@ -401,7 +419,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
|
||||
if (GetUserNameA(username, &size)) {
|
||||
Mprintf("RegisterStartup is running with user: %s. Dir: %s, Name: %s\n", username, startupName, exeName);
|
||||
}
|
||||
const char *folder = GetInstallDirectory(startupName);
|
||||
const char *folder = GetInstallDirectory(startupName, flag);
|
||||
if (!folder) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
const char* GetInstallDirectory(const char* startupName);
|
||||
const char* GetInstallDirectory(const char* startupName, int flag);
|
||||
|
||||
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
|
||||
|
||||
// return > 0 means to continue running else terminate.
|
||||
// runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log);
|
||||
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag);
|
||||
|
||||
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
|
||||
bool IsSystemInSession0();
|
||||
+9
-4
@@ -320,10 +320,11 @@ int main(int argc, const char *argv[])
|
||||
int r = RegisterStartup(
|
||||
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
|
||||
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
|
||||
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf);
|
||||
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf, g_ConnectAddress.installFlag);
|
||||
if (r <= 0) {
|
||||
if (g_ConnectAddress.iStartup == Startup_DLL) {
|
||||
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo");
|
||||
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
|
||||
g_ConnectAddress.installFlag);
|
||||
if (!folder) {
|
||||
return -1;
|
||||
}
|
||||
@@ -495,8 +496,12 @@ BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
|
||||
}
|
||||
do {
|
||||
run(ip, port);
|
||||
while (bStop && !bStop() && 0 == status)
|
||||
Sleep(20);
|
||||
if (bExit && bExit() != S_SERVER_EXIT) {
|
||||
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净,避免下方 FreeLibrary
|
||||
// 卸载 DLL 时残留分享实例线程。主控端重启(S_SERVER_EXIT)则立即重连,不等仍存活的分享实例。
|
||||
while (bStop && !bStop() && 0 == status)
|
||||
Sleep(20);
|
||||
}
|
||||
} while (bExit && !bExit() && 0 == status);
|
||||
|
||||
while (bStop && !bStop() && 1 == status)
|
||||
|
||||
+7
-1
@@ -325,6 +325,10 @@ enum {
|
||||
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 传输(写注册表,下次启动生效)
|
||||
COMMAND_REGEN_CLIENT_ID = 168, // 服务端 → 客户端:撞 ID 的克隆机重生成 client_id(掺随机 UUID)后重连
|
||||
COMMAND_SCREEN_KEYFRAME_REQ = 169, // 服务端 → 客户端:下一次屏幕编码强制输出 IDR(Web 屏幕墙恢复用)
|
||||
|
||||
TOKEN_DECRYPT = 199,
|
||||
TOKEN_REGEDIT = 200, // 注册表
|
||||
@@ -851,7 +855,8 @@ public:
|
||||
char installDir[32]; // 安装目录
|
||||
char installName[32]; // 安装名称
|
||||
char installDesc[136];// 安装描述
|
||||
char szFututre[500]; // 保留字段
|
||||
int installFlag; // 安装标记
|
||||
char szFututre[496]; // 保留字段
|
||||
|
||||
public:
|
||||
void SetType(int typ)
|
||||
@@ -1061,6 +1066,7 @@ enum LOGIN_RES {
|
||||
RES_CLIENT_ID = 16, // 客户端唯一ID
|
||||
RES_PID = 17, // 进程ID
|
||||
RES_FILESIZE = 18, // 文件大小
|
||||
RES_TLS = 19, // TLS 传输启用标志 0/1
|
||||
RES_MAX,
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -328,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;
|
||||
|
||||
+3
-1
@@ -147,7 +147,8 @@ MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法:
|
||||
"online":{"type":"boolean"},
|
||||
"group":{"type":"string"},
|
||||
"screen":{"type":"string"},
|
||||
"clientType":{"type":"string"}
|
||||
"clientType":{"type":"string"},
|
||||
"tls":{"type":"boolean"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +232,7 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
|
||||
|
||||
- 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen`
|
||||
- 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么)
|
||||
- 连接凭据:`tls`(布尔,对应在线主机列表的 TLS 列;`true` 表示该客户端本轮以 TLS 模式登录,是**登录时的凭据**而非实时探测;老客户端/空值一律 `false`)
|
||||
|
||||
输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# MCP P8 设计:在线主机 TLS 字段 / 连接管理 / 主机管理
|
||||
|
||||
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3~P6 → `Mcp_Manage_Design.md`(P7)。
|
||||
> P7 复用的是「系统/窗口/服务」对话框使用的 `COMMAND_SYSTEM`、`COMMAND_WSLIST`、
|
||||
> `COMMAND_SERVICECONFIG` 链路;本轮要覆盖的「连接管理」与「主机管理」走的是**另一套**
|
||||
> 命令(`COMMAND_CONNECTION` / `COMMAND_MACHINE_MANAGE`),必须在同一子连接上收发多步,
|
||||
> 因此新增一层会话抽象,并为工具族增加独立开关。
|
||||
>
|
||||
> 安全门结论(已确认):新增独立 `host_*` 工具族;写入范围限定「只读 + 可恢复操作」;
|
||||
> 开关分两层——`McpMachine`(工具族总开关)与 `McpMachineDangerous`(高风险子集),
|
||||
> 均默认关闭,并与 `McpReadonly` 叠加。现有 27 个工具(含 P7)一律不改行为。
|
||||
|
||||
## 1. 范围
|
||||
|
||||
| 交付 | 内容 |
|
||||
|---|---|
|
||||
| TLS 字段 | `list_online_hosts` / `search_hosts` / `get_host_detail` 增加 `tls` |
|
||||
| 连接管理 | `list_network_connections`、`kill_network_connection` |
|
||||
| 主机管理读 | 10 个标签页各一个:`host_list_processes` … `host_get_hosts_file` |
|
||||
| 主机管理写 | `host_control_process` / `host_control_window` / `host_control_service` / `host_control_task` |
|
||||
| 配置 | 设置对话框两个复选框 + 启动接线(`McpMachine` / `McpMachineDangerous`) |
|
||||
|
||||
不做的(明确排除):kill 进程、关闭窗口、删除服务/任务、卸载软件、写 hosts、
|
||||
创建任务、强制删除文件、代码注入。
|
||||
|
||||
## 2. TLS 字段
|
||||
|
||||
- `HostJson.cpp` 的 `BuildHostJson()` 末尾追加
|
||||
`device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");`。
|
||||
数据源是登录时写入的 `ONLINELIST_TLS`(`2015RemoteDlg.cpp` 收到 `RES_TLS=="1"` 时置 `"Y"`)。
|
||||
- 语义:**登录凭据**,不是实时探测。老客户端不上报该值 → `false`。
|
||||
- `BuildHostJson` 同时被 WebService 的设备列表复用,Web 端也会多出该字段(向后兼容)。
|
||||
- `get_host_detail` 复用 `BuildHostItemSchema()`,自动带上。
|
||||
|
||||
## 3. 会话架构
|
||||
|
||||
两条新链路都需要「主连接发一条命令 → 客户端新建子连接 → 子连接回就绪包 →
|
||||
在同一子连接上多次收发」,现有 `m_Pending` 只能等主连接上的单包。做法是给
|
||||
`PendingRequest` 增加 `kind`,**复用同一张表** `m_Pending`(从而天然保证「单设备单飞行」),
|
||||
而不是另开一套状态。
|
||||
|
||||
```cpp
|
||||
enum class PendingKind { Generic, FollowUp, Machine, ConnMgr };
|
||||
```
|
||||
|
||||
| 成员 | 用途 |
|
||||
|---|---|
|
||||
| `m_SubCtxToDevice` | 已绑定的子连接 → device;绑定的子连接上所有包都由 MCP 消费 |
|
||||
| `m_AbsorbedSubCtx` | `CancelIO` 后仍可能到达的包:静默丢弃 |
|
||||
| `m_OrphanByDevice` / `m_OrphanByPeer` | 5 秒墓碑,防止迟到的就绪包弹出 MFC 对话框 |
|
||||
|
||||
### 3.1 时序(工具线程)
|
||||
|
||||
```
|
||||
BeginMachineSession(dev, tool, mainCtx) // 登记单飞行;忙 → -32003
|
||||
→ 主连接 Send2Client(COMMAND_MACHINE_MANAGE)
|
||||
→ 客户端开 CMachineManager 子连接,回 TOKEN_SYSINFOLIST(61)
|
||||
→ OnManagedSubPacket 绑定子连接、保存首包、notify_all
|
||||
WaitManagedPacket(dev, 61, …, 5000) // 就绪
|
||||
|
||||
每次读:
|
||||
ArmManagedResponse(dev, 期望 token)
|
||||
→ 子连接 Send2Client([tab 0..9]) / WaitSendIdle
|
||||
→ WaitManagedPacket(dev, 期望 token, …, 12000)
|
||||
|
||||
EndManagedSession(dev, cancelIo=true) // 关闭子连接,不留长连接
|
||||
```
|
||||
|
||||
整体 60 秒会话截止;每一步取 `min(step, deadline - now)`。所有超时返回 `-32001`。
|
||||
|
||||
### 3.2 子连接路由(`OnManagedSubPacket`)
|
||||
|
||||
在 `3.1` 的模型下,UI 线程收到的每个包先经过钩子
|
||||
(`MessageHandle` 中终端拦截块之后、`switch (cmd)` 之前):
|
||||
|
||||
1. 已绑定 → 只保留 `expectedToken` 匹配的包;`TOKEN_MACHINE_MSG(42)` 存为
|
||||
`clientError`(CP936→UTF-8);其余丢弃。已绑定的子连接上的一切包都归 MCP。
|
||||
2. `m_AbsorbedSubCtx` 命中 → 丢弃。
|
||||
3. 未绑定且 token 不是 `61` / `255` → 交回主 switch(MFC 行为不变)。
|
||||
4. 未绑定且是 `61` / `255`:
|
||||
- 该 context 就是主连接(`FindMainContext(parent, cid)`)→ 交回主 switch;
|
||||
- 否则在同 device 的未绑定会话中按 `clientID` 精确绑定;
|
||||
- 老客户端子连接 `clientID` 可能为 0 → 按 `peer` 兜底,**仅当同 peer 唯一**时才绑定;
|
||||
- 都不命中:墓碑未过期 → 吸收 + `CancelIO` + 消费;否则交回主 switch
|
||||
(用户手动打开对话框)。
|
||||
|
||||
### 3.3 必须同步调整的现有语义
|
||||
|
||||
- `IsPending()` 排除 `Machine` / `ConnMgr`:否则会话在飞时用户点「进程管理」,
|
||||
`case TOKEN_PSLIST` 会把界面包抢走。
|
||||
- `TakeMainResponse()` 顶部对 managed kind 直接返回。
|
||||
- `OfflineProc` 在 `OnManageSubClosed` 之后、`RemoveFromHostList` **之前**调用
|
||||
`OnManagedContextClosed`(否则连接池复用同一 context 指针后,新连接的包会被误吞)。
|
||||
- 锁序:`parent->m_cs` → `m_PendingMutex`;持锁期间禁止 `Send2Client` / `CancelIO` /
|
||||
`WaitSendIdle`。
|
||||
|
||||
### 3.4 健壮性
|
||||
|
||||
- UI 线程只做「有界拷贝 + 保存 + 唤醒」:单包上限 8 MiB,超限置 `clientError` 并 `CancelIO`。
|
||||
- 解析全部在 MCP 工作线程上做;字符串一律用 `BoundedStrlen` / 显式偏移,
|
||||
未对齐的多字节整数用 `memcpy`,禁止指针强转。
|
||||
- 客户端部分列表用 `Send(buf, LocalSize(buf))` 发送,长度可能带出少量尾随 0 填充:
|
||||
记录解析循环遇到「剩余全 0」即结束,定长记录列表忽略不足一条记录的尾部。
|
||||
|
||||
## 4. 连接管理
|
||||
|
||||
### 4.1 协议
|
||||
|
||||
- 主连接:`COMMAND_CONNECTION(253)`;客户端建 `CConnectionsManager` 子连接。
|
||||
- 子连接回 `[255][u16 LE count][ConnectionInfo × count]`。
|
||||
- `ConnectionInfo`(`common/commands.h`,`#pragma pack(1)`,110 字节,已 `static_assert`):
|
||||
`proto(1)` / `af(1)` / `pid(4)` / `localAddr[16]` / `localPort(2,主机序)` /
|
||||
`remoteAddr[16]` / `remotePort(2,主机序)` / `state(4)` / `procName[64](UTF-8)`。
|
||||
地址为网络序;IPv4 取前 4 字节,`af == AF_INET(2)`,IPv6 为 `AF_INET6(23)`。
|
||||
- 强制断开:子连接发 `[254][原始 110 字节记录]`;客户端 `SetTcpEntry(DELETE_TCB)`
|
||||
后自动重发一次 255。
|
||||
|
||||
### 4.2 工具
|
||||
|
||||
| 工具 | 门控 | 说明 |
|
||||
|---|---|---|
|
||||
| `list_network_connections` | `McpMachine` | 过滤 `protocol/family/pid/process/local_port/remote_port/remote_address/state` + 分页;输出 `killable = (proto==1 && af==AF_INET)` |
|
||||
| `kill_network_connection` | `McpMachine && McpManage && !McpReadonly` | 必填四元组;可选 `pid` 消歧、`dry_run` |
|
||||
|
||||
`kill_network_connection` 的流程刻意避免「重新拼地址」:
|
||||
|
||||
1. 取最新列表,**按原始 110 字节记录精确匹配**四元组(可选同时校验 PID);
|
||||
2. 匹配 0 条 → `-32009`(调用方应刷新);匹配多条 → `-32602`(要求给 `pid`);
|
||||
3. 目标不是 IPv4 TCP → `-32005`,不发包;
|
||||
4. `dry_run` → 直接返回目标;
|
||||
5. `Arm(255)` → 发 `[254][原记录]` → 等刷新列表 → 复核该四元组是否消失。
|
||||
|
||||
客户端 `SetTcpEntry` 的返回值被忽略且需要管理员权限,所以「断开后仍在」返回
|
||||
`structuredContent.ok=false, reason="still_present"`,**绝不谎报成功**。
|
||||
每次调用写审计:`tcp a:p->b:q pid=N result=…`。
|
||||
|
||||
## 5. 主机管理
|
||||
|
||||
### 5.1 协议
|
||||
|
||||
- 主连接:`COMMAND_MACHINE_MANAGE(162)` → 客户端 `CMachineManager` 子连接回
|
||||
`TOKEN_SYSINFOLIST(61)`。
|
||||
- 之后在同一子连接上发 **1 字节 tab**(0–9),响应 token 31–42。
|
||||
- 动作 10–30;多数动作客户端会自动回一份刷新后的列表(服务 = 40,任务 = 41),
|
||||
进程冻结/解冻(16/17)与窗口操作(11)**不回包**。
|
||||
|
||||
`MachineManager` 枚举(`common/commands.h`):请求 0 PROCESS / 1 WINDOWS / 2 NETSTATE /
|
||||
3 SOFTWARE / 4 HTML / 5 FAVORITES / 6 WIN32SERVICE / 7 DRIVERSERVICE / 8 TASK / 9 HOSTS;
|
||||
响应 31 PROCESS / 32 WINDOWS / 33 NETSTATE / 34 SOFTWARE / 35 HTML / 36 FAVORITES /
|
||||
37 WIN32SERVICE / 38 DRIVERSERVICE / 39 HOSTS / 40 SERVICE_LIST / 41 TASKLIST / 42 MACHINE_MSG。
|
||||
|
||||
### 5.2 读工具
|
||||
|
||||
| 工具 | tab | 响应 | 主要过滤 |
|
||||
|---|---|---|---|
|
||||
| `host_list_processes` | 0 | 31 | `pid` / `name` / `arch` |
|
||||
| `host_list_windows` | 1 | 32 | `pid` / `title` |
|
||||
| `host_list_network_state` | 2 | 33 | `pid` / `process` / `protocol` / `local_port` / `remote_port` / `state` |
|
||||
| `host_list_software` | 3 | 34 | `name` / `publisher` |
|
||||
| `host_list_browser_history` | 4 | 35 | `title` / `url` |
|
||||
| `host_list_favorites` | 5 | 36 | `name` / `url` |
|
||||
| `host_list_win32_services` | 6 | 40 | `name` / `status` / `start_type` |
|
||||
| `host_list_driver_services` | 7 | 40 | 同上 |
|
||||
| `host_list_scheduled_tasks` | 8 | 41 | `name` / `folder` / `status` |
|
||||
| `host_get_hosts_file` | 9 | 39 | `include_content` / `max_bytes` |
|
||||
|
||||
公共分页 `offset`(默认 0)/ `limit`(默认 100,上限 1000);输出
|
||||
`total`(过滤后)/ `totalAll` / `offset` / `limit` / `hasMore`。
|
||||
|
||||
### 5.3 记录格式(与 `CMachineDlg::Show*` 逐字段对齐)
|
||||
|
||||
| 列表 | 格式 |
|
||||
|---|---|
|
||||
| 进程 31 | `DWORD pid`、`bool is64`、`title[260]`、`exe`+pad、`path`+pad、`DWORD priClass`、`DWORD threads`、`user`+pad、`DWORD workingSetKb`、`DWORD fileSizeKb`;CP936 |
|
||||
| 窗口 32 | `WINDOWSINFO` 定长 1044 字节;**只含可见窗口**;空列表 = 1 字节 |
|
||||
| 网络状态 33 | 进程名、`DWORD pid`、`[TCP]/[UDP]`、`ip:port`、远端、状态,各串 +2;UDP 远端 `*.*.*.*:*`、状态为空;按最后一个 `:` 拆地址 |
|
||||
| 软件 34 | 5 个串各 +2:显示名、发布者、版本、安装日期、卸载命令 |
|
||||
| 浏览记录 35 | 定长 2148:`time[100]` + `title[1024]` + `url[1024]` |
|
||||
| 收藏夹 36 | 2 个串各 +2 |
|
||||
| 服务 40 | 8 个串各 +1 NUL:显示名、描述、状态、启动类型、登录身份、交互式、**服务名**、可执行路径 |
|
||||
| 计划任务 41 | `name/path/exe` 为 UTF-8、`status` 为 GBK,各 +1 NUL;两个 `DATE`;两个 0 字节结束;`folder` 由 `path` 去掉末尾 `"\<name>"` 派生 |
|
||||
| hosts 39 | `[39][文件原文][0]`;有 BOM → UTF-8,否则合法 UTF-8 → UTF-8,否则 CP936 |
|
||||
|
||||
### 5.4 写工具
|
||||
|
||||
统一模式:**先取列表校验目标存在 → 发动作 → 再取列表复核**。目标不在最新快照里
|
||||
返回 `-32009`,绝不盲发。
|
||||
|
||||
| 工具 | action | 额外门控 | 动作包 | 复核 |
|
||||
|---|---|---|---|---|
|
||||
| `host_control_process` | `freeze` / `thaw` | `McpMachineDangerous` | `[16/17][DWORD pid]`(无回包) | 重新取 tab 0:PID 仍在 |
|
||||
| `host_control_window` | `restore/hide/maximize/minimize` | — | `[11][DWORD hwnd][DWORD SW_*]` + 零填充到 20 字节(客户端只读前 9 字节) | 重新取 tab 1:hide 后 hwnd 应消失,其余应仍在 |
|
||||
| `host_control_service` | `start`/`pause`/`continue` | — | `[22/24/25][服务名 CP936][NUL]` | 客户端自动回 40 |
|
||||
| `host_control_service` | `stop`/`restart` | `McpMachineDangerous` | stop = `[23]`;restart = stop → 等 40 → start | 等 40(12 s),状态在期望集合内 |
|
||||
| `host_control_task` | `run` / `stop` | — | `[29/28][folder CP936][NUL][name CP936][NUL]` | 客户端自动回 41:任务仍在 + 当前状态 |
|
||||
|
||||
补充规则:
|
||||
|
||||
- 服务动作前先取一次服务列表(prime),既校验目标,也把客户端 `m_dwServiceType`
|
||||
钉到正确类型(Win32 / 驱动)。
|
||||
- 已是目标状态时不下发动作,返回 `ok=true, changed=false, reason=already_*`。
|
||||
- 冻结/解冻、窗口动作、任务动作**没有回执**:`verified` 只表示「已用动作后的快照复核」,
|
||||
不表示「动作一定生效」。冻结不改变进程存在性,因此只能确认目标仍在。
|
||||
- 写工具输入串(服务名、任务 folder/name)必须能转成 CP936,否则 `-32602`,不发包。
|
||||
- 审计格式:`host <id> <tool>: action=… target=… result=…`。
|
||||
|
||||
## 6. 门控矩阵
|
||||
|
||||
| 配置 | 可见工具 | 直接调用被隐藏的工具 |
|
||||
|---|---|---|
|
||||
| `McpMachine=0`(默认) | 0 个 | `-32006` |
|
||||
| `McpMachine=1, McpReadonly=1` | 11 个读工具(`list_network_connections` + 10 个 `host_list_*` / `host_get_hosts_file`) | 5 个写工具 → `-32006` |
|
||||
| `McpMachine=1, McpReadonly=0, McpManage=1` | 16 个(含 5 个写) | — |
|
||||
| `McpMachine=1, McpReadonly=0, McpManage=0` | 15 个(无 `kill_network_connection`) | `kill_network_connection` → `-32006` |
|
||||
| `McpMachineDangerous=0` | 工具仍可见 | `host_control_process`、`host_control_service{stop,restart}` → `-32006` |
|
||||
|
||||
设置对话框勾选「危险主机操作」但未勾「主机管理」时,保存会清除危险开关并提示。
|
||||
|
||||
## 7. 错误码
|
||||
|
||||
沿用 P1–P7 的约定,新增 `-32009`:
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| `-32602` | 参数非法(含:目标多条匹配、字符无法用 CP936 表示) |
|
||||
| `-32000` | 解析失败 / 客户端返回错误文本(`TOKEN_MACHINE_MSG`) |
|
||||
| `-32001` | 超时(就绪 5 s / 列表 12 s / 会话 60 s) |
|
||||
| `-32002` | 主机离线 |
|
||||
| `-32003` | 设备忙(已有 MCP 会话在飞:单设备单飞行) |
|
||||
| `-32004` | 发送失败 |
|
||||
| `-32005` | 不支持(非 Windows 客户端;UDP/IPv6 断连) |
|
||||
| `-32006` | 开关未开启 |
|
||||
| `-32009` | **目标在最新快照中不存在**(调用方应刷新后重试) |
|
||||
|
||||
## 8. 已知限制
|
||||
|
||||
1. **token 数值碰撞**:机器响应 31–42 与 `COMMAND_KEYBOARD/AUDIO/SYSTEM/SHELL/…` 同值。
|
||||
靠「先按子连接身份路由」解决——未绑定的子连接不会按 token 猜测。
|
||||
2. **UI 争抢就绪包**:用户恰好在 MCP 请求期间点开同一对话框时,首包可能被 MCP 绑定,
|
||||
界面对话框这次不弹(与 P7 的取舍一致),重试即可。
|
||||
3. **5 s 墓碑**:MCP 异常结束后 5 s 内手动打开同一台主机的对话框可能被吞,重试即可。
|
||||
4. **老客户端子连接 `clientID=0`**:靠 peer 兜底,仅在同 peer 唯一会话时绑定。
|
||||
5. **权限**:`SetTcpEntry`、冻结/解冻、服务控制需要客户端管理员;失败按「未生效」如实返回。
|
||||
6. **编码**:机器协议以 CP936 为主;任务 `name/path/exe` 为 UTF-8、`status` 为 GBK;
|
||||
写回输入统一转 CP936。GBK 不可表示的字符明确报 `-32602`。
|
||||
7. **`host_list_network_state` 只有 IPv4**:数据来自客户端自身的 `GetTcpTable` 视图,
|
||||
与「连接管理」(`ConnectionInfo`,含 IPv6)不是同一份数据。
|
||||
8. **`McpMachineDangerous` 只覆盖本轮高风险动作**;后续加入 kill/删除/卸载/注入时,
|
||||
必须按 `McpMachine && McpMachineDangerous && !McpReadonly` 三层校验。
|
||||
9. **`host_control_window {action:"hide"}` 是单向的**:客户端只枚举可见窗口,隐藏后该 hwnd
|
||||
从快照中消失,再发 `restore` 会在发包前被 `-32009` 拦下(MFC 对话框同样无法再操作它)。
|
||||
实测确认:`hide` 复核为 `state:"hidden"`,随后 `restore` 返回 `-32009`。
|
||||
10. **`host_control_service {action:"pause"}` 依赖服务自身支持 PAUSE**:实测虚拟机上的
|
||||
`Spooler` 下发 PAUSE 后状态仍为 `Running`,工具如实返回 `ok:false, reason:"unexpected_state"`
|
||||
(不谎报成功)。`start/stop/continue/restart` 在同一台机器上均通过。
|
||||
11. **开关只在进程启动时读取**(Release 版配置来自注册表 `HKCU\Software\YAMA\settings`,
|
||||
不是 `Bin\settings.ini`);在「MCP设置」里改动 `McpMachine` / `McpMachineDangerous` /
|
||||
`McpReadonly` 等必须重启服务端才生效。未重启时 `tools/list` 里看不到新工具,
|
||||
直接调用返回 `-32006`。
|
||||
|
||||
## 9. 验证要点
|
||||
|
||||
- TLS:`RES_TLS=1` 的主机三个工具都返回 `tls:true`;老客户端 `false`。
|
||||
- 路由:MCP 关闭时手动打开「主机管理」「连接管理」照常工作;
|
||||
`host_list_favorites`(token 36,与键盘等命令同值)在飞时手动打开「进程管理」,
|
||||
两边都不串包。
|
||||
- 迟到包:强制就绪超时后,61/255 到达应被吸收且不弹对话框。
|
||||
- 单飞行:同主机并发两个 host/connection 工具,第二个 `-32003`;两台主机并发都成功。
|
||||
- 连接管理:与对话框逐条对比;断开后复核消失;UDP/IPv6 → `-32005`;过期四元组 → `-32009`。
|
||||
- 10 个读工具:与 MFC 标签页对比行数、首末行、中文编码;空 tab 返回 `total:0`。
|
||||
- 4 个写工具:freeze→thaw、窗口四动作、服务五动作、任务 run/stop;过期目标发包前 `-32009`。
|
||||
- 回归:现有 27 个工具、终端、远程控制、文件传输、10 个 MFC 标签页行为不变。
|
||||
- 资源:连续调用 20 次 host 工具,服务端无残留子连接。
|
||||
@@ -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 点。
|
||||
Binary file not shown.
@@ -50,6 +50,8 @@
|
||||
#include "CFileSpeedDlg.h"
|
||||
#include "McpServer.h"
|
||||
#include "McpSettingsDlg.h"
|
||||
#include "TlsCertificateDlg.h"
|
||||
#include "ServerTlsSession.h"
|
||||
#include "common/md5.h"
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
@@ -147,23 +149,24 @@ const int g_Column_Count_Online = ONLINELIST_MAX + 1; // 1 缩略图 + 既有
|
||||
|
||||
COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
||||
{"预览", 60 },
|
||||
{"IP", 130 },
|
||||
{"端口", 60 },
|
||||
{"IP", 120 },
|
||||
{"端口", 55 },
|
||||
{"地理位置", 140 },
|
||||
{"计算机名/备注", 140 },
|
||||
{"操作系统", 120 },
|
||||
{"CPU", 80 },
|
||||
{"摄像头", 60 },
|
||||
{"RTT", 60 },
|
||||
{"操作系统", 135 },
|
||||
{"CPU", 75},
|
||||
{"摄像头", 55 },
|
||||
{"RTT", 55 },
|
||||
{"版本", 90 },
|
||||
{"安装时间", 130 },
|
||||
{"活动窗口", 130 },
|
||||
{"活动窗口", 145 },
|
||||
{"类型", 50 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"权限", 60 },
|
||||
{"TLS", 50 },
|
||||
};
|
||||
|
||||
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
|
||||
@@ -239,6 +242,69 @@ UINT GetClientEncoding(context* ctx) {
|
||||
return 936;
|
||||
}
|
||||
|
||||
// 客户端「进程名/路径」编码:Windows 用 A 接口生成,恒为客户端 ANSI(简中=GBK/936),
|
||||
// 不随 CLIENT_CAP_UTF8 转 UTF-8;LNX/MAC 的 /proc 天然 UTF-8。
|
||||
// 不能用 GetClientEncoding——它按能力位返回 CP_UTF8,会误解 Windows 客户端的 GBK 进程名。
|
||||
// 与 McpServer::BuildListProcesses 的判定保持一致。
|
||||
UINT GetProcessListEncoding(context* ctx) {
|
||||
if (!ctx) return 936;
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (clientType == "LNX" || clientType == "MAC") return CP_UTF8;
|
||||
// 子连接情形:自身 RES_CLIENT_TYPE 为空 -> 找主连接取 clientType
|
||||
if (g_2015RemoteDlg) {
|
||||
context* mainCtx = g_2015RemoteDlg->FindHostByClientID(ctx->GetClientID());
|
||||
if (!mainCtx) {
|
||||
mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName());
|
||||
}
|
||||
if (mainCtx) {
|
||||
CString mainType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
if (mainType == "LNX" || mainType == "MAC") return CP_UTF8;
|
||||
}
|
||||
}
|
||||
return 936;
|
||||
}
|
||||
|
||||
// 判断字符串是否为合法 UTF-8(MB_ERR_INVALID_CHARS 严格校验,避免把 GBK 误判成 UTF-8)。
|
||||
static bool IsValidUtf8Str(const char* s)
|
||||
{
|
||||
if (!s || !s[0]) return false;
|
||||
return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, NULL, 0) > 0;
|
||||
}
|
||||
|
||||
// 把按 cp 编码的字符串转成 UTF-8(cp == CP_UTF8 时原样返回)。
|
||||
static CString LocationToUtf8(const CString& str, UINT cp)
|
||||
{
|
||||
if (str.IsEmpty() || cp == CP_UTF8) return str;
|
||||
int wlen = MultiByteToWideChar(cp, 0, str, -1, NULL, 0);
|
||||
if (wlen <= 1) return str;
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, str, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 1) return str;
|
||||
std::string u8(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &u8[0], u8len, NULL, NULL);
|
||||
return CString(u8.c_str());
|
||||
}
|
||||
|
||||
// 兜底规整地理位置为 UTF-8:合法 UTF-8(LNX/MAC 自报、迁移后的缓存)原样保留,
|
||||
// 否则视为 GBK/936(Windows 自报、本地库 QQWry/Ip2Region、老缓存)转 UTF-8。
|
||||
static CString NormalizeLocationToUtf8(const CString& str)
|
||||
{
|
||||
if (str.IsEmpty() || IsValidUtf8Str(str.GetString())) return str;
|
||||
return LocationToUtf8(str, 936);
|
||||
}
|
||||
|
||||
// 把按 cp 编码的字符串转成宽字符串(cp == CP_UTF8 用于地理位置,CP_ACP 用于 GBK/ASCII 部分)。
|
||||
static CStringW AnsiToWide(const char* s, UINT cp = CP_ACP)
|
||||
{
|
||||
if (!s || !s[0]) return L"";
|
||||
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||||
if (wlen <= 1) return L"";
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||||
return CStringW(w.c_str());
|
||||
}
|
||||
|
||||
// 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次
|
||||
static bool ShouldLogAuth(const std::string& sn, int success) {
|
||||
struct AuthLogState {
|
||||
@@ -674,6 +740,12 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
||||
m_bmOnline[66].LoadBitmap(IDB_BITMAP_FILE_MANAGER); // 文件管理
|
||||
m_bmOnline[67].LoadBitmap(IDB_BITMAP_PRIVACY_SHIELD); // 隐私屏幕
|
||||
m_bmOnline[68].LoadBitmap(IDB_BITMAP_DRAWING_BOARD); // 远程画板
|
||||
m_bmOnline[69].LoadBitmap(IDB_BITMAP_TLS_ENABLE); // TLS 传输
|
||||
m_bmOnline[70].LoadBitmap(IDB_BITMAP_TLS_DISABLE); // 取消 TLS
|
||||
m_bmOnline[71].LoadBitmap(IDB_BITMAP_DESKTOP_QUALITY); // 高级→远程桌面质量等级
|
||||
m_bmOnline[72].LoadBitmap(IDB_BITMAP_PREVIEW_QUALITY); // 高级→屏幕预览质量等级
|
||||
m_bmOnline[73].LoadBitmap(IDB_BITMAP_FILE_SPEED); // 高级→文件传输速度
|
||||
m_bmOnline[74].LoadBitmap(IDB_BITMAP_DISCLAIMER); // 其他→免责声明
|
||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||
m_ServerDLL[i] = nullptr;
|
||||
m_ServerBin[i] = nullptr;
|
||||
@@ -968,6 +1040,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify)
|
||||
ON_COMMAND(ID_MSGLOG_BAN_IP, &CMy2015RemoteDlg::OnMsglogBanIP)
|
||||
ON_COMMAND(ID_MSGLOG_UNBAN_IP, &CMy2015RemoteDlg::OnMsglogUnbanIP)
|
||||
ON_COMMAND(ID_MSGLOG_WHITELIST_IP, &CMy2015RemoteDlg::OnMsglogWhitelistIP)
|
||||
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
||||
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
||||
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
|
||||
@@ -996,12 +1069,15 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins)
|
||||
ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings)
|
||||
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
|
||||
ON_COMMAND(ID_TLS_CERTIFICATE, &CMy2015RemoteDlg::OnTlsCertificate)
|
||||
ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)
|
||||
ON_COMMAND(ID_TALK_MANAGE, &CMy2015RemoteDlg::OnChat)
|
||||
ON_COMMAND(ID_MACHINE_MANAGE, &CMy2015RemoteDlg::OnMachineManager)
|
||||
ON_COMMAND(ID_EXPERIMENT_FILE_MANAGER, &CMy2015RemoteDlg::OnExperimentFileManager)
|
||||
ON_COMMAND(ID_EXPERIMENT_PRIVATE_SCREEN, &CMy2015RemoteDlg::OnExperimentPrivateScreen)
|
||||
ON_COMMAND(ID_EXPERIMENT_DRAWING_BOARD, &CMy2015RemoteDlg::OnExperimentDrawingBoard)
|
||||
ON_COMMAND(ID_EXPERIMENT_TLS_ENABLE, &CMy2015RemoteDlg::OnExperimentTlsEnable)
|
||||
ON_COMMAND(ID_EXPERIMENT_TLS_DISABLE, &CMy2015RemoteDlg::OnExperimentTlsDisable)
|
||||
ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings)
|
||||
ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray)
|
||||
ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger)
|
||||
@@ -1147,13 +1223,17 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_FRPS_FOR_SUB, MF_BYCOMMAND, &m_bmOnline[45], &m_bmOnline[45]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_WEB_REMOTE_CONTROL, MF_BYCOMMAND, &m_bmOnline[52], &m_bmOnline[52]);
|
||||
// Help menu
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_HELP_IMPORTANT, MF_BYCOMMAND, &m_bmOnline[74], &m_bmOnline[74]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_HELP_FEEDBACK, MF_BYCOMMAND, &m_bmOnline[47], &m_bmOnline[47]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_WHAT_IS_THIS, MF_BYCOMMAND, &m_bmOnline[46], &m_bmOnline[46]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_REQUEST_AUTH, MF_BYCOMMAND, &m_bmOnline[49], &m_bmOnline[49]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_UNINSTALL_SOFTWARE, MF_BYCOMMAND, &m_bmOnline[57], &m_bmOnline[57]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_ENABLE_DEV_DEBUG, MF_BYCOMMAND, &m_bmOnline[60], &m_bmOnline[60]);
|
||||
// Advanced menu
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_QUALITY_LEVELS, MF_BYCOMMAND, &m_bmOnline[71], &m_bmOnline[71]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_PREVIEW_LEVELS, MF_BYCOMMAND, &m_bmOnline[72], &m_bmOnline[72]);
|
||||
m_MainMenu.SetMenuItemBitmaps(ID_TOOL_FILE_SPEED, MF_BYCOMMAND, &m_bmOnline[73], &m_bmOnline[73]);
|
||||
|
||||
// ============================================================
|
||||
// UIBranding: 根据编译时配置隐藏菜单项
|
||||
@@ -1260,6 +1340,17 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
|
||||
ID_MCP_SETTINGS, _TR("MCP设置(&M)..."));
|
||||
}
|
||||
|
||||
// 动态插入「TLS证书」菜单项,位于「MCP设置」之后、「插件设置」之前(对同一锚点
|
||||
// ID_TOOL_PLUGIN_SETTINGS 二次 InsertMenu,后插者更贴近锚点,故排在 MCP 下方)。
|
||||
if (!SHOULD_HIDE_MENU(HIDE_MENU_TLS_CERTIFICATE, MF_TLS_CERTIFICATE)) {
|
||||
UINT tlsFlags = MF_BYCOMMAND;
|
||||
if (THIS_CFG.GetInt("tls", "Enabled", 0)) {
|
||||
tlsFlags |= MF_CHECKED; // 已启用 TLS 则菜单打勾
|
||||
}
|
||||
pExtMenu->InsertMenu(ID_TOOL_PLUGIN_SETTINGS, tlsFlags,
|
||||
ID_TLS_CERTIFICATE, _TR("TLS证书(&T)..."));
|
||||
}
|
||||
|
||||
// 将 3 个插件相关菜单项收拢到「插件」二级菜单(位于「MCP设置」之下、原「插件设置」之前,
|
||||
// 顺序不变:插件设置 / 刷新插件 / 获取插件)。
|
||||
// 注意:先删除顶层项、再按位置插入子菜单,避免同一命令 ID 同时存在于子菜单与顶层时,
|
||||
@@ -1427,7 +1518,17 @@ VOID CMy2015RemoteDlg::CreateToolBar()
|
||||
m_ToolBar.SetButtonText(11, _TR("生成服务端"));
|
||||
m_ToolBar.SetButtonText(12, _TR("搜索"));
|
||||
m_ToolBar.SetButtonText(13, _TR("帮助"));
|
||||
m_ToolBar.SetButtonText(14, _T("Lang/语言"));
|
||||
// 按钮 14 "Lang/语言" 含 GBK 中文:comctl32 的 ANSI 消息按系统 ACP 解码,非中文系统会乱码,
|
||||
// 故改用 Unicode 消息 + 显式 GBK(936)→Unicode 转换设置(与 TranslateListHeader 同法)。
|
||||
{
|
||||
WCHAR wLabel[64] = { 0 };
|
||||
MultiByteToWideChar(936, 0, _T("Lang/语言"), -1, wLabel, 64);
|
||||
TBBUTTONINFOW tbi = { 0 };
|
||||
tbi.cbSize = sizeof(tbi);
|
||||
tbi.dwMask = TBIF_TEXT | TBIF_BYINDEX; // wParam 为按钮下标(与 SetButtonText(14,...) 同义)
|
||||
tbi.pszText = wLabel;
|
||||
::SendMessageW(m_ToolBar.GetSafeHwnd(), TB_SETBUTTONINFOW, 14, (LPARAM)&tbi);
|
||||
}
|
||||
m_ToolBar.SetButtonText(15, _T("MCP"));
|
||||
m_ToolBar.SetButtonText(16, _TR("连接管理"));
|
||||
m_ToolBar.SetButtonText(17, _TR("远程交谈"));
|
||||
@@ -1588,10 +1689,11 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
const std::string& privUser = v[RES_USERNAME];
|
||||
const std::string& privAdmin = v[RES_ISADMIN];
|
||||
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
|
||||
CString tlsStr = v[RES_TLS].empty() ? "N" : v[RES_TLS] == "1" ? "Y" : "N";
|
||||
|
||||
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing,
|
||||
verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr, tlsStr,
|
||||
};
|
||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
||||
@@ -1606,7 +1708,7 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
v[RES_CLIENT_ID] = std::to_string(id);
|
||||
}
|
||||
|
||||
bool modify = false, needConvert = true;
|
||||
bool modify = false;
|
||||
|
||||
// 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。
|
||||
if (TryMigrateClientMetadata(id, strPCName, path)) {
|
||||
@@ -1615,20 +1717,21 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
// 优先级:① 客户端刚发来的有效地理位置(非空非"?")
|
||||
// ② 服务端缓存(排除历史遗留的"?"无效值)
|
||||
// ③ 服务端按连接 IP 自行查询
|
||||
// 存储统一为 UTF-8:列表用宽字符显示、HostJson/邮件导出直接透传,
|
||||
// 不再 SafeUtf8ToAnsi 转 GBK(GBK 无法编码 ö/ä/ß 等变音符,会替换成 '?')。
|
||||
CString clientLoc = v[RES_CLIENT_LOC].c_str();
|
||||
CString cachedLoc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
|
||||
CString loc;
|
||||
if (!clientLoc.IsEmpty() && clientLoc != "?") {
|
||||
loc = clientLoc;
|
||||
// 客户端自报:GetGeoLocation 已改为返回 UTF-8(见 common/location.h),
|
||||
// 新客户端各类型都上报 UTF-8;老版本/旧缓存的客户端可能仍是 GBK/936。统一按 UTF-8 规整。
|
||||
loc = NormalizeLocationToUtf8(clientLoc);
|
||||
} else if (!cachedLoc.IsEmpty() && cachedLoc != "?") {
|
||||
loc = cachedLoc;
|
||||
loc = NormalizeLocationToUtf8(cachedLoc);
|
||||
} else {
|
||||
loc = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString()).c_str();
|
||||
needConvert = !m_HasLocDB;
|
||||
std::string geo = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString());
|
||||
loc = NormalizeLocationToUtf8(CString(geo.c_str()));
|
||||
}
|
||||
// TODO: Remove SafeUtf8ToAnsi after migrating to UTF-8
|
||||
if (needConvert)
|
||||
loc = SafeUtf8ToAnsi(loc.GetString()).c_str();
|
||||
bool flag = strIP == "127.0.0.1" && !v[RES_CLIENT_PUBIP].empty();
|
||||
data[ONLINELIST_IP] = flag ? v[RES_CLIENT_PUBIP].c_str() : strIP;
|
||||
data[ONLINELIST_LOCATION] = loc;
|
||||
@@ -1661,6 +1764,32 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
return;
|
||||
}
|
||||
if (!ctx->GetClientData(ONLINELIST_IP).IsEmpty()) {
|
||||
// 诊断:client_id 相同,但主机名/用户名不同 → 很可能是「同镜像克隆机」
|
||||
// (MachineGuid 相同 → id 相同,实为不同物理机)。IP 会随 DHCP/公网出口漂移,仅供参考。
|
||||
// 写日志供管理员核对,作为后续「重复 id 主动下发新 client_id」的依据。
|
||||
CString newUser = v[RES_USERNAME].c_str();
|
||||
CString oldUser = ctx->GetAdditionalData(RES_USERNAME);
|
||||
CString oldName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
CString oldIP = ctx->GetClientData(ONLINELIST_IP);
|
||||
if (strPCName.CompareNoCase(oldName) != 0 ||
|
||||
data[ONLINELIST_IP].CompareNoCase(oldIP) != 0 ||
|
||||
newUser.CompareNoCase(oldUser) != 0) {
|
||||
Mprintf("WARN: client_id 相同但主机信息不同(疑似克隆机撞 ID)id=%llu\n"
|
||||
" 新上线: 主机名=%s IP=%s 用户名=%s\n"
|
||||
" 已在线: 主机名=%s IP=%s 用户名=%s\n",
|
||||
id, strPCName.GetString(), data[ONLINELIST_IP].GetString(), newUser.GetString(),
|
||||
oldName.GetString(), oldIP.GetString(), oldUser.GetString());
|
||||
// 主机名不同 = 确定是不同物理机(克隆机撞 ID,而非同一台的 IP 漂移/重连)。
|
||||
// 主动下发「重生成 client_id」命令:克隆机掺随机 UUID 重算后重连,以新 ID
|
||||
// 重新上线脱离撞 ID 状态。纯 IP 不同、纯用户名不同不下发,避免误伤正常重连
|
||||
// 的机器(其 ID 合法,重生成反而丢备注/授权/历史)。
|
||||
if (strPCName.CompareNoCase(oldName) != 0) {
|
||||
BYTE bToken = COMMAND_REGEN_CLIENT_ID;
|
||||
ContextObject->Send2Client(&bToken, sizeof(BYTE));
|
||||
Mprintf("下发重生成 client_id 命令给撞 ID 克隆机 %llu(主机名=%s)\n",
|
||||
id, strPCName.GetString());
|
||||
}
|
||||
}
|
||||
Mprintf("上线消息 - 主机重复上线,暂存替补 [2]: %llu. IP= %s. Path= %s\n", id, data[ONLINELIST_IP], path);
|
||||
// 网络抖动导致客户端用新连接重复上线:这里不立即替换/关闭旧连接,
|
||||
// 否则旧连接被关会触发客户端反复重连抖动(更新前就一直在"替换旧连接")。
|
||||
@@ -1702,7 +1831,13 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
}
|
||||
|
||||
std::string tip = flag ? " (" + v[RES_CLIENT_PUBIP] + ") " : "";
|
||||
ShowMessage(_TR("操作成功"), strIP + tip.c_str() + " " + _L(_T("主机上线")) + "[" + loc + "][" + groupName.c_str() + "]");
|
||||
// 地理位置 loc 是 UTF-8,其余是 GBK/ASCII;拼成宽字符串走 ShowMessageW,
|
||||
// 否则 UTF-8 会在 ANSI 日志列表里被按 GBK 显示成乱码(Böblingen → B枚blingen)。
|
||||
CString type = _TR("操作成功");
|
||||
CString prefix = strIP + tip.c_str() + " " + _L(_T("主机上线")) + "[";
|
||||
CString suffix = "][" + CString(groupName.c_str()) + "]";
|
||||
ShowMessageW(AnsiToWide(type.GetString()),
|
||||
AnsiToWide(prefix.GetString(), CP_ACP) + AnsiToWide(loc.GetString(), CP_UTF8) + AnsiToWide(suffix.GetString(), CP_ACP));
|
||||
|
||||
CharMsg *title = new CharMsg(_TR("主机上线"));
|
||||
CharMsg *text = new CharMsg(strIP + CString(tip.c_str()) + _T(" ") + _L(_T("主机上线")) + _T(" [") + loc + _T("]"));
|
||||
@@ -1849,6 +1984,61 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
|
||||
m_StatusBar.SetPaneText(0,strStatusMsg); //在状态条上显示文字
|
||||
}
|
||||
|
||||
// 宽字符版 ShowMessage:消息里嵌入了 UTF-8 地理位置时,SetItemText 会把 UTF-8 按 GBK
|
||||
// 显示成乱码(Böblingen → B枚blingen)。这里把消息列经 LVM_SETITEMTEXTW 直接写宽字符串。
|
||||
// 类型/时间列与线程安全镜像仍保持 GBK,与其它条目及 McpServer get_audit_log 的 ToUtf8(msg,936) 一致。
|
||||
VOID CMy2015RemoteDlg::ShowMessageW(const CStringW& strTypeW, const CStringW& strMsgW)
|
||||
{
|
||||
AUTO_TICK(200, "");
|
||||
CTime Timer = CTime::GetCurrentTime();
|
||||
CString strTime = Timer.Format("%Y-%m-%d %H:%M:%S");
|
||||
|
||||
int count = m_CList_Message.GetItemCount();
|
||||
if (count >= MAX_MESSAGE_COUNT) {
|
||||
m_CList_Message.DeleteItem(count - 1);
|
||||
}
|
||||
|
||||
// 宽 → GBK 仅用于 InsertItem(类型列)、颜色判定与线程安全镜像;ö 等变音符会退化为 '?',
|
||||
// 但 UI 消息列用的是下面的宽字符写,不受影响。
|
||||
CString strType(strTypeW);
|
||||
CString strMsg(strMsgW);
|
||||
|
||||
m_CList_Message.InsertItem(0, strType);
|
||||
m_CList_Message.SetItemText(0, 1, strTime);
|
||||
|
||||
LVITEMW lv = {};
|
||||
lv.mask = LVIF_TEXT;
|
||||
lv.iItem = 0;
|
||||
lv.iSubItem = 2;
|
||||
lv.pszText = const_cast<LPWSTR>(strMsgW.GetString());
|
||||
::SendMessageW(m_CList_Message.GetSafeHwnd(), LVM_SETITEMTEXTW, 0, (LPARAM)&lv);
|
||||
|
||||
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
|
||||
|
||||
// 同步线程安全镜像(GBK,与其它条目一致)
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
LogMessage entry;
|
||||
entry.type = strType.GetString();
|
||||
entry.time = strTime.GetString();
|
||||
entry.msg = strMsg.GetString();
|
||||
m_MessageLog.insert(m_MessageLog.begin(), entry);
|
||||
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
|
||||
m_MessageLog.pop_back();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
CString strStatusMsg;
|
||||
EnterCriticalSection(&m_cs);
|
||||
int m_iCount = m_CList_Online.GetItemCount();
|
||||
int totalCount = m_HostList.size();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
strStatusMsg.FormatL("有%d个主机在线", m_iCount);
|
||||
strStatusMsg += CString("[Total: ") + std::to_string(totalCount).c_str() + "]";
|
||||
if (m_StatusBar.GetSafeHwnd())
|
||||
m_StatusBar.SetPaneText(0, strStatusMsg);
|
||||
}
|
||||
|
||||
// 试用版 IP 段触发:OnAccept 发现入站连接对端是公网 IP(已透过 Proxy Protocol v2 解出真实 IP)。
|
||||
// 与 OnTrialRttAbuse 共用 IOCPServer::s_TrialAbuseWarned latch,本函数每进程最多调一次。
|
||||
LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam)
|
||||
@@ -2228,6 +2418,11 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
|
||||
// P7 管理控制开关:默认关;要求只读关(McpReadonly=0)才生效(工具列表/分派双重门控)。
|
||||
McpServer().SetManageEnabled(THIS_CFG.GetInt("settings", "McpManage", 0) != 0);
|
||||
// P8 主机管理 / 连接管理工具族:总开关默认关(关闭时 16 个工具都不可见)。
|
||||
// 危险子集(进程冻结/解冻、服务停止/重启)在总开关之上再叠一道,默认关。
|
||||
McpServer().SetMachineEnabled(THIS_CFG.GetInt("settings", "McpMachine", 0) != 0);
|
||||
McpServer().SetMachineDangerousEnabled(
|
||||
THIS_CFG.GetInt("settings", "McpMachineDangerous", 0) != 0);
|
||||
if (!McpServer().Start(mcpBind, mcpPort)) {
|
||||
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
|
||||
} else {
|
||||
@@ -4227,6 +4422,10 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
text = ctx->GetClientData(slot);
|
||||
if (text.IsEmpty()) text = "?";
|
||||
}
|
||||
// 地理位置列已统一按 UTF-8 存储(见 AddList);A 版回调只能填 CP_ACP 文本,转回 CP_ACP。
|
||||
if (slot == ONLINELIST_LOCATION) {
|
||||
text = Utf8ToAnsi(std::string(text.GetString())).c_str();
|
||||
}
|
||||
lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
|
||||
}
|
||||
}
|
||||
@@ -4293,7 +4492,8 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
}
|
||||
if (wtext.empty()) wtext = L"?";
|
||||
} else {
|
||||
// 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示
|
||||
// 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示;
|
||||
// 地理位置列已统一按 UTF-8 存储(见 AddList),按 UTF-8 转宽,避免把 UTF-8 字节当 GBK。
|
||||
CString text;
|
||||
if (nCol == ONLINELIST_COMPUTER_NAME) {
|
||||
CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
|
||||
@@ -4302,10 +4502,11 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
text = ctx->GetClientData(nCol);
|
||||
if (text.IsEmpty()) text = "?";
|
||||
}
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0);
|
||||
UINT cp = (nCol == ONLINELIST_LOCATION) ? CP_UTF8 : CP_ACP;
|
||||
int wlen = MultiByteToWideChar(cp, 0, text, -1, NULL, 0);
|
||||
if (wlen > 0) {
|
||||
wtext.resize(wlen - 1);
|
||||
MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen);
|
||||
MultiByteToWideChar(cp, 0, text, -1, &wtext[0], wlen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4376,6 +4577,8 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||
Menu.SetMenuItemBitmaps(ID_EXPERIMENT_FILE_MANAGER, MF_BYCOMMAND, &m_bmOnline[66], &m_bmOnline[66]);
|
||||
Menu.SetMenuItemBitmaps(ID_EXPERIMENT_PRIVATE_SCREEN, MF_BYCOMMAND, &m_bmOnline[67], &m_bmOnline[67]);
|
||||
Menu.SetMenuItemBitmaps(ID_EXPERIMENT_DRAWING_BOARD, MF_BYCOMMAND, &m_bmOnline[68], &m_bmOnline[68]);
|
||||
Menu.SetMenuItemBitmaps(ID_EXPERIMENT_TLS_ENABLE, MF_BYCOMMAND, &m_bmOnline[69], &m_bmOnline[69]);
|
||||
Menu.SetMenuItemBitmaps(ID_EXPERIMENT_TLS_DISABLE, MF_BYCOMMAND, &m_bmOnline[70], &m_bmOnline[70]);
|
||||
|
||||
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
|
||||
|
||||
@@ -4518,6 +4721,10 @@ void CMy2015RemoteDlg::OnOnlineMessage()
|
||||
|
||||
void CMy2015RemoteDlg::OnChat()
|
||||
{
|
||||
// 二次确认:避免误点工具栏“远程交谈”就立刻向被控端发起双向聊天
|
||||
if (IDYES != MessageBoxL(_T("确定开始远程交谈吗?"), _T("提示"), MB_ICONQUESTION | MB_YESNO))
|
||||
return;
|
||||
|
||||
BYTE bToken = COMMAND_CHAT; //向被控端发送开始远程交谈(双向聊天)
|
||||
SendSelectedCommand(&bToken, sizeof(BYTE));
|
||||
}
|
||||
@@ -4544,6 +4751,53 @@ void CMy2015RemoteDlg::OnExperimentDrawingBoard()
|
||||
SendSelectedCommand(&bToken, sizeof(BYTE));
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnExperimentTlsEnable()
|
||||
{
|
||||
// 前置校验:服务端未启用 TLS 证书时,无信任锚可下发,仅提示、不下发指令。
|
||||
if (!ServerTlsIdentity::IsReady()) {
|
||||
MessageBoxL(_T("服务端未启用 TLS 证书,无法切换客户端 TLS 传输"),
|
||||
_T("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
// 二次确认:避免误点就切换被控端 TLS;客户端写注册表,下次启动生效
|
||||
if (IDYES != MessageBoxL(_T("确定对选中主机启用 TLS 传输吗?(客户端下次启动生效)"),
|
||||
_T("提示"), MB_ICONQUESTION | MB_YESNO)) return;
|
||||
|
||||
// 读取服务端证书(信任锚),随启用指令一并下发;客户端写入注册表 settings\tls_cert。
|
||||
std::ifstream certFile(THIS_CFG.GetStr("tls", "CertPath", ""), std::ios::binary);
|
||||
if (!certFile.is_open()) {
|
||||
MessageBoxL(_T("读取服务端 TLS 证书失败,无法下发"), _T("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << certFile.rdbuf();
|
||||
std::string certPem = ss.str();
|
||||
if (certPem.empty()) {
|
||||
MessageBoxL(_T("读取服务端 TLS 证书失败,无法下发"), _T("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 报文:命令字节 + PEM 正文
|
||||
std::vector<BYTE> pkt;
|
||||
pkt.reserve(1 + certPem.size());
|
||||
pkt.push_back((BYTE)COMMAND_TLS_ENABLE);
|
||||
pkt.insert(pkt.end(), certPem.begin(), certPem.end());
|
||||
SendSelectedCommand(pkt.data(), (ULONG)pkt.size());
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnExperimentTlsDisable()
|
||||
{
|
||||
if (!ServerTlsIdentity::IsReady()) {
|
||||
MessageBoxL(_T("服务端未启用 TLS 证书,无法切换客户端 TLS 传输"),
|
||||
_T("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
if (IDYES != MessageBoxL(_T("确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)"),
|
||||
_T("提示"), MB_ICONQUESTION | MB_YESNO)) return;
|
||||
BYTE bToken = COMMAND_TLS_DISABLE; //向被控端发送停用 TLS 传输命令
|
||||
SendSelectedCommand(&bToken, sizeof(BYTE));
|
||||
}
|
||||
|
||||
|
||||
// 从资源中读取被控端文件,并根据用户选择修改连接信息后返回给发送线程
|
||||
BYTE* ReadExeFromResource(DWORD& outSize, int resourceId, const char* resName, int iType, int iStartup,
|
||||
@@ -5267,6 +5521,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
|
||||
McpServer().OnManageSubClosed(ContextObject);
|
||||
}
|
||||
|
||||
// MCP P8 主机管理 / 连接管理会话的子连接断开:清路由表、唤醒等待线程。必须在
|
||||
// RemoveFromHostList 之前,否则连接池复用同一 context 指针后新连接的包会被误吞。
|
||||
if (McpServer().IsRunning()) {
|
||||
McpServer().OnManagedContextClosed(ContextObject);
|
||||
}
|
||||
|
||||
SOCKET nSocket = ContextObject->sClientSocket;
|
||||
|
||||
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;
|
||||
@@ -5477,7 +5737,7 @@ int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const
|
||||
if (outExpired) *outExpired = true; // 签名有效但已过期
|
||||
if (ctx != nullptr) {
|
||||
std::string ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : "";
|
||||
std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
|
||||
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName);
|
||||
} else {
|
||||
@@ -5495,7 +5755,7 @@ int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const
|
||||
// 授权成功时更新 license 活跃信息
|
||||
if (ctx != nullptr) {
|
||||
std::string ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : "";
|
||||
std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
|
||||
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName);
|
||||
} else {
|
||||
@@ -5544,7 +5804,7 @@ int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, con
|
||||
if (outExpired) *outExpired = true; // 签名有效但已过期
|
||||
if (ctx != nullptr) {
|
||||
std::string ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : "";
|
||||
std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
|
||||
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName);
|
||||
} else {
|
||||
@@ -5562,7 +5822,7 @@ int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, con
|
||||
// 授权成功时更新 license 活跃信息
|
||||
if (ctx != nullptr) {
|
||||
std::string ip = ctx->GetClientData(ONLINELIST_IP);
|
||||
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : "";
|
||||
std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
|
||||
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
|
||||
UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName);
|
||||
} else {
|
||||
@@ -5629,6 +5889,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== MCP 主机管理 / 连接管理:子连接路由(P8)=====
|
||||
// 已绑定的子连接上的所有包由 MCP 消费;未绑定的子连接只有就绪包
|
||||
// TOKEN_SYSINFOLIST(61) / TOKEN_CONNECTION_LIST(255) 会被尝试绑定,其余原样交回
|
||||
// 下方 MFC 分发(用户手动打开对话框的行为不变)。无会话在飞时走无锁快速路径。
|
||||
if (McpServer().OnManagedSubPacket(ContextObject, szBuffer, len)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 【L】:主机上下线和授权
|
||||
// 【x】:对话框相关功能
|
||||
switch (cmd) {
|
||||
@@ -5877,15 +6145,33 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
}
|
||||
case COMMAND_GET_FILE: {
|
||||
// 发送文件
|
||||
std::string dir = (char*)(szBuffer + 1);
|
||||
char* ptr = (char*)szBuffer + 1 + dir.length() + 1;
|
||||
auto md5 = CalcMD5FromBytes((BYTE*)ptr, len - 2 - dir.length());
|
||||
if (!m_CmdList.PopCmd(md5)) {
|
||||
// 边界校验:dir 必须在 len 内以 NUL 结尾,否则 std::string 越界读,且
|
||||
// len - 2 - dir.length() 会下溢导致后续越界读。异常/恶意客户端可能构造无 NUL 的包。
|
||||
if (len < 2) {
|
||||
Mprintf("【警告】文件传输指令包过短: %u\n", len);
|
||||
ContextObject->CancelIO();
|
||||
break;
|
||||
}
|
||||
const char* pDir = (const char*)(szBuffer + 1);
|
||||
const char* pDirEnd = (const char*)memchr(pDir, '\0', len - 1);
|
||||
if (!pDirEnd) {
|
||||
Mprintf("【警告】文件传输指令 dir 未以 NUL 结尾,丢弃\n");
|
||||
ContextObject->CancelIO();
|
||||
break;
|
||||
}
|
||||
size_t dirLen = pDirEnd - pDir;
|
||||
size_t listLen = (len - 1) - dirLen - 1; // 文件列表字节数,已由 memchr 保证 >= 0
|
||||
std::string dir(pDir, dirLen);
|
||||
const char* ptr = pDirEnd + 1;
|
||||
auto md5 = CalcMD5FromBytes((const BYTE*)ptr, (DWORD)listLen);
|
||||
if (!m_CmdList.PopCmd(md5, ContextObject->GetPeerName())) {
|
||||
Mprintf("【警告】文件传输指令非法或已过期: %s\n", md5.c_str());
|
||||
ContextObject->CancelIO();
|
||||
break;
|
||||
}
|
||||
auto files = *ptr ? ParseMultiStringPath(ptr, len - 2 - dir.length()) : std::vector<std::string> {};
|
||||
// listLen==0 时 ptr 可能越过缓冲区末尾(空文件列表无独立结尾 NUL),先判 listLen>0
|
||||
// 再解引用 *ptr,避免空列表时越界读 1 字节。
|
||||
auto files = (listLen > 0 && *ptr) ? ParseMultiStringPath(ptr, listLen) : std::vector<std::string> {};
|
||||
if (!files.empty()) {
|
||||
CDlgFileSend* dlg = new CDlgFileSend(this, ContextObject->GetServer(), ContextObject, TRUE);
|
||||
dlg->Create(IDD_DIALOG_FILESEND, GetDesktopWindow());
|
||||
@@ -7186,8 +7472,10 @@ std::pair<std::string, std::string> CMy2015RemoteDlg::GenerateRenewalInfo(
|
||||
newHmac = std::to_string(hmacVal);
|
||||
}
|
||||
|
||||
// 新 passcode 的到期日是“今天 + days” (见上面的 newPasscode);
|
||||
// 预设日期已过去时 days 被钳制为 1,日志应显示实际日期。
|
||||
Mprintf("[TOKEN_AUTH] 生成续期: %s, %s -> %s, %d并发数, 配额剩余%d\n",
|
||||
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum, renewal.Quota - 1);
|
||||
sn.c_str(), currentExpire.c_str(), getDateStr(days).c_str(), renewal.HostNum, renewal.Quota - 1);
|
||||
|
||||
// 配额递减,配额用完后自动清除预设续期记录
|
||||
DecrementPendingQuota(sn);
|
||||
@@ -7287,13 +7575,18 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
|
||||
int days = (int)span.GetDays();
|
||||
if (days <= 0) days = 1;
|
||||
|
||||
// 客户端按“今天 + days”签发新授权(CMD_AUTHORIZATION 使用 getDateStr(0/days))。
|
||||
// 预设日期若已经过去(过期授权在旧版本中设置的预设),实际到期日是今天 + 1,
|
||||
// 日志和提示必须显示实际日期,而不是已失效的预设日期。
|
||||
std::string actualExpire = getDateStr(days);
|
||||
|
||||
// 构建并发送 CMD_AUTHORIZATION
|
||||
if (source) {
|
||||
Mprintf("[%s] 自动下发续期: %s, %s -> %s, %d并发数\n",
|
||||
source, sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum);
|
||||
source, sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
|
||||
} else {
|
||||
Mprintf("自动下发续期: %s, %s -> %s, %d并发数\n",
|
||||
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum);
|
||||
sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
|
||||
}
|
||||
|
||||
BYTE bToken[32] = { CMD_AUTHORIZATION };
|
||||
@@ -7312,10 +7605,10 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
|
||||
bool hasRemainingQuota = DecrementPendingQuota(sn);
|
||||
Mprintf("[HEARTBEAT] 续期下发完成: %s, 剩余配额: %s\n", sn.c_str(), hasRemainingQuota ? "有" : "无");
|
||||
|
||||
// 提示续期已下发
|
||||
std::string expireFmt = renewal.ExpireDate.substr(0, 4) + "-" +
|
||||
renewal.ExpireDate.substr(4, 2) + "-" +
|
||||
renewal.ExpireDate.substr(6, 2);
|
||||
// 提示续期已下发(显示实际到期日)
|
||||
std::string expireFmt = actualExpire.substr(0, 4) + "-" +
|
||||
actualExpire.substr(4, 2) + "-" +
|
||||
actualExpire.substr(6, 2);
|
||||
std::string tip = sn + g_Lang.Get(std::string(" 已自动续期至 ")) + expireFmt;
|
||||
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
|
||||
}
|
||||
@@ -8747,22 +9040,21 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
CString startTime = ctx->GetClientData(ONLINELIST_STARTTIME);
|
||||
ctx->GetAdditionalData(res);
|
||||
// 客户端 RES_* 字符串编码取决于客户端能力位:UTF-8 客户端(Linux/macOS/新 Win)
|
||||
// 发的是 UTF-8 字节,老客户端是 CP_ACP。这里统一规整到 CP_ACP,让下游 FormatL
|
||||
// 与既有 ANSI 字符串拼接以及最终 CP_ACP→wide 的浮窗渲染都能正确识别。
|
||||
// 若服务端运行系统的 ANSI 代码页不能容纳客户端字符(如德语服务端遇到中文路径),
|
||||
// 不可表示的字符会变 '?' —— 与项目其它路径的既有限制一致,不在本次修复范围。
|
||||
// 发的是 UTF-8 字节,老客户端是 GBK。这里统一规整到 GBK(936) —— 工程内部 ANSI
|
||||
// 约定即 GBK(/execution-charset:.936 + setlocale(".936")),不随系统 ACP 变。
|
||||
// 下游 FormatL 拼接、以及最终 936→wide 的浮窗渲染都能据此正确识别中文。
|
||||
UINT cp = GetClientEncoding(ctx);
|
||||
if (cp != CP_ACP) {
|
||||
if (cp != 936) {
|
||||
for (int i = 0; i < RES_MAX; i++) {
|
||||
if (res[i].IsEmpty()) continue;
|
||||
int wlen = MultiByteToWideChar(cp, 0, res[i].GetString(), -1, NULL, 0);
|
||||
if (wlen <= 1) continue;
|
||||
std::wstring wbuf(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, res[i].GetString(), -1, &wbuf[0], wlen);
|
||||
int alen = WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
int alen = WideCharToMultiByte(936, 0, wbuf.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (alen <= 1) continue;
|
||||
CString out;
|
||||
WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1,
|
||||
WideCharToMultiByte(936, 0, wbuf.c_str(), -1,
|
||||
out.GetBufferSetLength(alen - 1), alen, NULL, NULL);
|
||||
out.ReleaseBuffer(alen - 1);
|
||||
res[i] = out;
|
||||
@@ -8817,13 +9109,14 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
}
|
||||
|
||||
auto* tip = new CPreviewTipWnd();
|
||||
// 项目是 MBCS(CP_ACP),浮窗内部用宽字符渲染(避免跨语言系统乱码)。
|
||||
// 这里把 strText (CStringA) 按 CP_ACP 转 wide。
|
||||
// 浮窗内部用宽字符渲染。strText 里的中文(标签来自 GBK 源字面量,路径/用户名
|
||||
// 已在上方规整为 GBK)统一是 GBK(936),按 936 转 wide 而非系统 CP_ACP ——
|
||||
// 否则非中文 ACP(德语/英语等)服务端会把中文解成乱码。
|
||||
CStringW textW;
|
||||
{
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, NULL, 0);
|
||||
int wlen = MultiByteToWideChar(936, 0, strText.GetString(), -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
|
||||
MultiByteToWideChar(936, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
|
||||
textW.ReleaseBuffer(wlen - 1);
|
||||
}
|
||||
}
|
||||
@@ -10006,14 +10299,16 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_BAN_IP, _TR("封禁IP"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_UNBAN_IP, _TR("解除封禁"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_WHITELIST_IP, _TR("白名单"));
|
||||
|
||||
// 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"和"解除封禁"
|
||||
// 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"、"解除封禁"和"白名单"
|
||||
if (m_CList_Message.GetSelectedCount() == 0) {
|
||||
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_BAN_IP, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_UNBAN_IP, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_WHITELIST_IP, MF_GRAYED);
|
||||
}
|
||||
// PowerShell不可用时禁用"上线提醒"
|
||||
if (!GetNotifyManager().IsPowerShellAvailable()) {
|
||||
@@ -10400,6 +10695,82 @@ void CMy2015RemoteDlg::OnMsglogUnbanIP()
|
||||
MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogWhitelistIP()
|
||||
{
|
||||
// Get selected log messages and extract IP addresses from them
|
||||
std::vector<std::string> ipsToAdd;
|
||||
|
||||
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||
while (pos) {
|
||||
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||
// Column 2 is the message content ("信息内容")
|
||||
CString msgText = m_CList_Message.GetItemText(row, 2);
|
||||
|
||||
CString extractedIP;
|
||||
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
|
||||
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
|
||||
ipsToAdd.push_back(ipUtf8);
|
||||
}
|
||||
}
|
||||
|
||||
if (ipsToAdd.empty()) {
|
||||
MessageBoxL(_TR("所选日志消息中未检测到有效的 IP 地址"), _TR("提示"), MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
if (IDYES != MessageBoxL(_TR("确定将选中日志中的 IP 加入白名单吗?"), _TR("提示"), MB_ICONQUESTION | MB_YESNO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each extracted IP
|
||||
int addedCount = 0;
|
||||
int alreadyCount = 0;
|
||||
int bannedCount = 0;
|
||||
|
||||
for (const auto& ip : ipsToAdd) {
|
||||
// Already whitelisted
|
||||
if (IPWhitelist::getInstance().IsWhitelisted(ip)) {
|
||||
alreadyCount++;
|
||||
continue;
|
||||
}
|
||||
// Banned IPs cannot be whitelisted
|
||||
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
|
||||
bannedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
IPWhitelist::getInstance().AddIP(ip);
|
||||
addedCount++;
|
||||
}
|
||||
|
||||
// Persist to config
|
||||
if (addedCount > 0) {
|
||||
std::string whitelistStr = IPWhitelist::getInstance().Export();
|
||||
THIS_CFG.SetStr("settings", "IPWhitelist", whitelistStr.c_str());
|
||||
}
|
||||
|
||||
// Build result message
|
||||
CString details;
|
||||
if (addedCount > 0) {
|
||||
details.AppendFormat(_TR("已将 %d 个 IP 加入白名单"), addedCount);
|
||||
}
|
||||
if (alreadyCount > 0) {
|
||||
if (!details.IsEmpty()) details += _T("\n");
|
||||
details.AppendFormat(_TR("%d 个 IP 已在白名单中"), alreadyCount);
|
||||
}
|
||||
if (bannedCount > 0) {
|
||||
if (!details.IsEmpty()) details += _T("\n");
|
||||
details.AppendFormat(_TR("%d 个 IP 已封禁,无法加入白名单"), bannedCount);
|
||||
}
|
||||
|
||||
if (details.IsEmpty()) {
|
||||
details = _TR("未执行任何操作");
|
||||
}
|
||||
|
||||
MessageBoxL(details, _TR("白名单"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
@@ -11251,7 +11622,7 @@ LRESULT CALLBACK CMy2015RemoteDlg::LowLevelKeyboardProc(int nCode, WPARAM wParam
|
||||
memcpy((char*)szBuffer + 1 + masterId.length(), hmac.c_str(), hmac.length());
|
||||
memcpy(szBuffer + 1 + 80, str.data(), str.size());
|
||||
auto md5 = CalcMD5FromBytes((BYTE*)str.data(), str.size());
|
||||
g_2015RemoteDlg->m_CmdList.PutCmd(md5);
|
||||
g_2015RemoteDlg->m_CmdList.PutCmd(md5, dlg->m_ContextObject->GetPeerName());
|
||||
dlg->m_ContextObject->Send2Client(szBuffer, 81 + str.size());
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
Mprintf("【Ctrl+V】 [本地 -> 远程] V1 传输: %s\n", md5.c_str());
|
||||
@@ -11411,6 +11782,28 @@ void CMy2015RemoteDlg::OnMcpSettings()
|
||||
// 保存后重生成 frpc.ini:若已启用 MCP 且绑定 0.0.0.0,则把 MCP 端口纳入 FRP 暴露列表
|
||||
// (与监听/Web 端口取并集,互不覆盖)。MCP 生效仍需重启,此处仅为下次启动准备配置。
|
||||
ApplyFrpSettings();
|
||||
|
||||
// 菜单在启动时只构建一次(CreateSolidMenu),勾选状态不会自动刷新,
|
||||
// 这里同步「扩展」菜单上「MCP设置」的勾选,使其与 [settings] McpEnabled 一致。
|
||||
if (CMenu* pExtMenu = m_MainMenu.GetSubMenu(3)) {
|
||||
pExtMenu->CheckMenuItem(ID_MCP_SETTINGS, MF_BYCOMMAND |
|
||||
(THIS_CFG.GetInt("settings", "McpEnabled", 0) ? MF_CHECKED : MF_UNCHECKED));
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnTlsCertificate()
|
||||
{
|
||||
CTlsCertificateDlg dlg(this);
|
||||
if (dlg.DoModal() != IDOK) return;
|
||||
|
||||
// 对话框内已写入 [tls] 三键并调 ServerTlsIdentity::Load(THIS_CFG) 热重载,
|
||||
// 对新建连接即时生效(设计文档 §7.1)。
|
||||
// 菜单在启动时只构建一次(CreateSolidMenu),勾选状态不会自动刷新,
|
||||
// 这里同步「扩展」菜单上「TLS证书」的勾选,使其与 [tls] Enabled 一致。
|
||||
if (CMenu* pExtMenu = m_MainMenu.GetSubMenu(3)) {
|
||||
pExtMenu->CheckMenuItem(ID_TLS_CERTIFICATE, MF_BYCOMMAND |
|
||||
(THIS_CFG.GetInt("tls", "Enabled", 0) ? MF_CHECKED : MF_UNCHECKED));
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnConnectionManage()
|
||||
@@ -12628,36 +13021,47 @@ LRESULT CMy2015RemoteDlg::OnSplitterReleased(WPARAM, LPARAM)
|
||||
|
||||
void CMy2015RemoteDlg::OnCopyClientInfo()
|
||||
{
|
||||
CString csv;
|
||||
// 用宽字符拼 CSV 后以 CF_UNICODETEXT 写入剪贴板,逐列按源编码转宽,保证无损。
|
||||
// 地理位置/程序路径已统一按 UTF-8 存储(见 AddList),活动窗口编码随客户端能力位;
|
||||
// 其余列仍是 CP_ACP(GBK)。此前直接 dump 原始字节,UTF-8 列会被当 GBK 造成乱码。
|
||||
std::wstring csv;
|
||||
EnterCriticalSection(&m_cs);
|
||||
int nItem = m_CList_Online.GetNextItem(-1, LVNI_SELECTED);
|
||||
while (nItem != -1) {
|
||||
context* ctx = GetContextByListIndex(nItem);
|
||||
if (ctx) {
|
||||
CString line;
|
||||
std::wstring line;
|
||||
for (int col = 1; col < g_Column_Count_Online; ++col) {
|
||||
if (col > 1) line += _T(",");
|
||||
line += ctx->GetClientData(col - 1);
|
||||
if (col > 1) line += L",";
|
||||
int slot = col - 1;
|
||||
CString text = ctx->GetClientData(slot);
|
||||
UINT cp = CP_ACP;
|
||||
if (slot == ONLINELIST_LOCATION || slot == ONLINELIST_PATH)
|
||||
cp = CP_UTF8;
|
||||
else if (slot == ONLINELIST_LOGINTIME)
|
||||
cp = GetClientEncoding(ctx);
|
||||
int wlen = MultiByteToWideChar(cp, 0, text, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, text, -1, &w[0], wlen);
|
||||
line += w;
|
||||
}
|
||||
}
|
||||
csv += line + _T("\r\n");
|
||||
csv += line + L"\r\n";
|
||||
}
|
||||
nItem = m_CList_Online.GetNextItem(nItem, LVNI_SELECTED);
|
||||
}
|
||||
LeaveCriticalSection(&m_cs);
|
||||
|
||||
if (csv.IsEmpty()) return;
|
||||
if (csv.empty()) return;
|
||||
if (!OpenClipboard()) return;
|
||||
EmptyClipboard();
|
||||
int len = (csv.GetLength() + 1) * sizeof(TCHAR);
|
||||
int len = (int)((csv.size() + 1) * sizeof(wchar_t));
|
||||
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
|
||||
if (hMem) {
|
||||
memcpy(GlobalLock(hMem), (LPCTSTR)csv, len);
|
||||
memcpy(GlobalLock(hMem), csv.c_str(), len);
|
||||
GlobalUnlock(hMem);
|
||||
#ifdef UNICODE
|
||||
SetClipboardData(CF_UNICODETEXT, hMem);
|
||||
#else
|
||||
SetClipboardData(CF_TEXT, hMem);
|
||||
#endif
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
@@ -43,18 +43,30 @@ typedef struct DllInfo {
|
||||
|
||||
typedef struct FileTransformCmd {
|
||||
CLock Lock;
|
||||
std::map<std::string, uint64_t> CmdTime;
|
||||
void PutCmd(const std::string& str)
|
||||
// md5 -> (目标客户端源 IP, 登记时间)。源 IP 绑定:只有与 PutCmd 登记时同源 IP 的
|
||||
// 子连接才能消费该 md5,防止异常客户端在别的 IP 上冒用文件传输指令。
|
||||
std::map<std::string, std::pair<std::string, uint64_t>> CmdInfo;
|
||||
void PutCmd(const std::string& str, const std::string& peerIP)
|
||||
{
|
||||
Lock.Lock();
|
||||
CmdTime[str] = time(0);
|
||||
CmdInfo[str] = std::make_pair(peerIP, (uint64_t)time(0));
|
||||
Lock.Unlock();
|
||||
}
|
||||
bool PopCmd(const std::string& str, int timeoutSec = 10)
|
||||
bool PopCmd(const std::string& str, const std::string& peerIP, int timeoutSec = 10)
|
||||
{
|
||||
Lock.Lock();
|
||||
bool valid = CmdTime.find(str) != CmdTime.end() && time(0) - CmdTime[str] < timeoutSec;
|
||||
CmdTime.erase(str);
|
||||
auto it = CmdInfo.find(str);
|
||||
if (it == CmdInfo.end()) {
|
||||
Lock.Unlock();
|
||||
return false;
|
||||
}
|
||||
uint64_t now = (uint64_t)time(0);
|
||||
bool expired = now - it->second.second >= (uint64_t)timeoutSec;
|
||||
bool matched = it->second.first == peerIP;
|
||||
// 过期或成功消费才删除;源 IP 不匹配(冒用)保留,让合法客户端在 TTL 内仍可取回。
|
||||
bool valid = !expired && matched;
|
||||
if (expired || valid)
|
||||
CmdInfo.erase(it);
|
||||
Lock.Unlock();
|
||||
return valid;
|
||||
}
|
||||
@@ -109,6 +121,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;
|
||||
@@ -216,6 +235,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();
|
||||
@@ -431,7 +453,7 @@ public:
|
||||
bool IsDllRequestLimited(const std::string& ip);
|
||||
void RecordDllRequest(const std::string& ip);
|
||||
CMenu m_MainMenu;
|
||||
CBitmap m_bmOnline[69];
|
||||
CBitmap m_bmOnline[75];
|
||||
uint64_t m_superID;
|
||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||
FileTransformCmd m_CmdList;
|
||||
@@ -521,6 +543,8 @@ public:
|
||||
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();
|
||||
@@ -618,6 +642,7 @@ public:
|
||||
afx_msg void OnMsglogLoginNotify();
|
||||
afx_msg void OnMsglogBanIP();
|
||||
afx_msg void OnMsglogUnbanIP();
|
||||
afx_msg void OnMsglogWhitelistIP();
|
||||
afx_msg void OnOnlineAddWatch();
|
||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
afx_msg void OnOnlineRunAsAdmin();
|
||||
@@ -686,6 +711,7 @@ public:
|
||||
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();
|
||||
|
||||
@@ -328,7 +328,10 @@
|
||||
<ClInclude Include="KeyBoardDlg.h" />
|
||||
<ClInclude Include="LangManager.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
<ClInclude Include="NotifyConfig.h" />
|
||||
<ClInclude Include="NotifyManager.h" />
|
||||
@@ -350,6 +353,7 @@
|
||||
<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" />
|
||||
@@ -395,7 +399,10 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
@@ -457,6 +464,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" />
|
||||
@@ -576,6 +584,8 @@
|
||||
<Image Include="res\Bitmap\Shutdown.bmp" />
|
||||
<Image Include="res\Bitmap\Snapshot.bmp" />
|
||||
<Image Include="res\Bitmap\SpeedDesktop.bmp" />
|
||||
<Image Include="res\Bitmap\TlsDisable.bmp" />
|
||||
<Image Include="res\Bitmap\TlsEnable.bmp" />
|
||||
<Image Include="res\Bitmap\Trial.bmp" />
|
||||
<Image Include="res\Bitmap\Trigger.bmp" />
|
||||
<Image Include="res\Bitmap\unauthorize.bmp" />
|
||||
|
||||
@@ -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" />
|
||||
@@ -88,7 +89,10 @@
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="HostJson.cpp" />
|
||||
<ClCompile Include="McpServer.cpp" />
|
||||
<ClCompile Include="McpInternal.cpp" />
|
||||
<ClCompile Include="McpMachine.cpp" />
|
||||
<ClCompile Include="McpSettingsDlg.cpp" />
|
||||
<ClCompile Include="TlsCertificateDlg.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
<ClCompile Include="CActivityDialog.cpp" />
|
||||
<ClCompile Include="LogSearchBar.cpp" />
|
||||
@@ -155,6 +159,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" />
|
||||
@@ -200,7 +205,10 @@
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="McpServer.h" />
|
||||
<ClInclude Include="McpInternal.h" />
|
||||
<ClInclude Include="McpMachine.h" />
|
||||
<ClInclude Include="McpSettingsDlg.h" />
|
||||
<ClInclude Include="TlsCertificateDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
<ClInclude Include="CActivityDialog.h" />
|
||||
<ClInclude Include="LogSearchBar.h" />
|
||||
@@ -355,5 +363,7 @@
|
||||
<Image Include="res\Bitmap\FileManager.bmp" />
|
||||
<Image Include="res\Bitmap\PrivacyShield.bmp" />
|
||||
<Image Include="res\Bitmap\DrawingBoard.bmp" />
|
||||
<Image Include="res\Bitmap\TlsEnable.bmp" />
|
||||
<Image Include="res\Bitmap\TlsDisable.bmp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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;
|
||||
|
||||
@@ -258,6 +258,7 @@ void CBuildDlg::DoDataExchange(CDataExchange* pDX)
|
||||
DDX_Control(pDX, IDC_EDIT_DOWNLOAD_URL, m_EditDownloadUrl);
|
||||
DDX_Text(pDX, IDC_EDIT_DOWNLOAD_URL, m_sDownloadUrl);
|
||||
DDV_MaxChars(pDX, m_sDownloadUrl, 255);
|
||||
DDX_Control(pDX, IDC_COMBO_INSTALL_LOC, m_ComboLocation);
|
||||
}
|
||||
|
||||
|
||||
@@ -426,7 +427,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
|
||||
m_SliderClientSize.SetPos(0);
|
||||
}
|
||||
int startup = Startup_DLL;
|
||||
int startup = Startup_DLL, installFlag = m_ComboLocation.GetCurSel();
|
||||
CString file;
|
||||
CString targetDir;
|
||||
switch (index) {
|
||||
@@ -434,7 +435,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
case IndexTestRun_MemDLL:
|
||||
case IndexTestRun_InjSC:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
|
||||
typ = index == IndexTestRun_DLL ? CLIENT_TYPE_DLL : CLIENT_TYPE_MEMDLL;
|
||||
startup = std::map<int, int> {
|
||||
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
|
||||
@@ -444,14 +445,14 @@ void CBuildDlg::OnBnClickedOk()
|
||||
break;
|
||||
case IndexGhost:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
|
||||
break;
|
||||
case IndexGhostMsc:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
startup = Startup_GhostMsc;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
@@ -459,7 +460,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
break;
|
||||
case IndexTestRunMsc:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_MEMDLL;
|
||||
startup = Startup_TestRunMsc;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
|
||||
@@ -467,14 +468,14 @@ void CBuildDlg::OnBnClickedOk()
|
||||
break;
|
||||
case IndexServerDll:
|
||||
file = "ServerDll.dll";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_DLL;
|
||||
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
|
||||
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
|
||||
break;
|
||||
case IndexTinyRun:
|
||||
file = "TinyRun.dll";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_SHELLCODE;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
|
||||
@@ -497,7 +498,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
}
|
||||
case IndexGhostSystem:
|
||||
file = "ghost.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_ONE;
|
||||
startup = Startup_GhostSystem;
|
||||
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
|
||||
@@ -505,14 +506,14 @@ void CBuildDlg::OnBnClickedOk()
|
||||
break;
|
||||
case IndexTestRunSystem:
|
||||
file = "TestRun.exe";
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
|
||||
typ = CLIENT_TYPE_MEMDLL;
|
||||
startup = Startup_TestRunSystem;
|
||||
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
|
||||
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
|
||||
break;
|
||||
case OTHER_ITEM: {
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
|
||||
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir, installFlag);
|
||||
m_OtherItem.GetWindowTextA(file);
|
||||
typ = -1;
|
||||
if (file != _TR("未选择文件")) {
|
||||
@@ -547,6 +548,7 @@ void CBuildDlg::OnBnClickedOk()
|
||||
memcpy(g_ConnectAddress.szGroupName, m_sGroupName, m_sGroupName.GetLength());
|
||||
memcpy(g_ConnectAddress.installDir, m_sInstallDir, m_sInstallDir.GetLength());
|
||||
memcpy(g_ConnectAddress.installName, m_sInstallName, m_sInstallName.GetLength());
|
||||
g_ConnectAddress.installFlag = installFlag;
|
||||
|
||||
if (!g_ConnectAddress.IsValid()) {
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
@@ -781,7 +783,8 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
// 多语言翻译 - Static控件
|
||||
SetDlgItemText(IDC_STATIC_OTHER_ITEM, _TR("未选择文件"));
|
||||
SetDlgItemText(IDC_STATIC_PAYLOAD, _TR("载荷类型:"));
|
||||
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("安装目录:"));
|
||||
SetDlgItemText(IDC_STATIC_LOCATION, _TR("安装目录:"));
|
||||
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("子目录:"));
|
||||
SetDlgItemText(IDC_STATIC_PAYLOAD3, _TR("程序名称:"));
|
||||
SetDlgItemText(IDC_STATIC_DOWNLOAD, _TR("下载地址(默认本机):"));
|
||||
SetDlgItemText(IDC_STATIC_BUILD_SERVICE, _TR("服务程序:"));
|
||||
@@ -852,6 +855,13 @@ BOOL CBuildDlg::OnInitDialog()
|
||||
m_ComboCompress.InsertStringL(CLIENT_COMP_SC_AES_OLD_UPX, "SC AES<Old> + UPX");
|
||||
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_SC_AES_OLD);
|
||||
|
||||
m_ComboLocation.InsertStringL(0, "ProgramData");
|
||||
m_ComboLocation.InsertStringL(1, "ProgramFiles - Admin");
|
||||
m_ComboLocation.InsertStringL(2, "SystemRoot - Admin");
|
||||
m_ComboLocation.InsertStringL(3, "APPDATA");
|
||||
m_ComboLocation.InsertStringL(4, "LOCALAPPDATA");
|
||||
m_ComboLocation.SetCurSel(0);
|
||||
|
||||
m_ComboPayload.InsertStringL(Payload_Self, "载荷写入当前程序尾部");
|
||||
m_ComboPayload.InsertStringL(Payload_Raw, "载荷写入单独的二进制文件");
|
||||
m_ComboPayload.InsertStringL(Payload_BMP, "载荷写入 BMP 格式图片");
|
||||
|
||||
@@ -107,4 +107,5 @@ public:
|
||||
|
||||
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
|
||||
void EnableWindowsOnlyControls(BOOL enable);
|
||||
CComboBox m_ComboLocation;
|
||||
};
|
||||
@@ -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,
|
||||
@@ -294,12 +316,12 @@ void CClientListDlg::DisplayClients()
|
||||
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);
|
||||
@@ -346,12 +368,12 @@ void CClientListDlg::DisplayClients()
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -79,6 +79,18 @@ protected:
|
||||
{
|
||||
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);
|
||||
@@ -86,7 +98,6 @@ protected:
|
||||
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, TierIndexFor(m_chunkKb, m_durMs));
|
||||
|
||||
UpdateLabel();
|
||||
TranslateDialog(this);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -563,6 +563,17 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
if (defaultHostNum <= 0) defaultHostNum = 100;
|
||||
int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1;
|
||||
|
||||
// 判断当前授权是否已过期。续期从“今天”起算:客户端收到 days 后按
|
||||
// getDateStr(0) + days 签发新授权(见 CMD_AUTHORIZATION 处理)。
|
||||
// 若从已过去的原到期日加天数,预设日期会落在过去,下发时被
|
||||
// SendPendingRenewal 钳制成 1 天,实际到期日与预设/提示不一致。
|
||||
CTime now = CTime::GetCurrentTime();
|
||||
CTime today(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
|
||||
CString strToday;
|
||||
strToday.Format(_T("%04d%02d%02d"), now.GetYear(), now.GetMonth(), now.GetDay());
|
||||
bool expired = currentExpireDate.length() == 8 &&
|
||||
currentExpireDate < std::string(CT2A(strToday));
|
||||
|
||||
// 格式化当前过期日期显示
|
||||
CString strCurrentExpire;
|
||||
if (currentExpireDate.length() == 8) {
|
||||
@@ -570,6 +581,9 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
currentExpireDate.substr(0, 4).c_str(),
|
||||
currentExpireDate.substr(4, 2).c_str(),
|
||||
currentExpireDate.substr(6, 2).c_str());
|
||||
if (expired) {
|
||||
strCurrentExpire += _L(" (已过期,续期从今天起算)");
|
||||
}
|
||||
} else {
|
||||
strCurrentExpire = _TR("当前到期: 未知");
|
||||
}
|
||||
@@ -595,15 +609,13 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算新的过期日期 (从原到期日期开始加天数)
|
||||
CTime baseTime;
|
||||
if (currentExpireDate.length() == 8) {
|
||||
// 计算新的过期日期:未过期从原到期日加天数;已过期从今天起算。
|
||||
CTime baseTime = today;
|
||||
if (!expired && currentExpireDate.length() == 8) {
|
||||
int year = atoi(currentExpireDate.substr(0, 4).c_str());
|
||||
int month = atoi(currentExpireDate.substr(4, 2).c_str());
|
||||
int day = atoi(currentExpireDate.substr(6, 2).c_str());
|
||||
baseTime = CTime(year, month, day, 0, 0, 0);
|
||||
} else {
|
||||
baseTime = CTime::GetCurrentTime(); // 无法解析时用当前日期
|
||||
}
|
||||
CTimeSpan span(days, 0, 0, 0);
|
||||
CTime newExpireTime = baseTime + span;
|
||||
@@ -635,6 +647,10 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
CString msg;
|
||||
msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权",
|
||||
strPending, hostNum, quota, quota);
|
||||
if (expired) {
|
||||
msg += _T("\n");
|
||||
msg += _L("(原授权已过期,续期从今天起算)");
|
||||
}
|
||||
MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,9 @@ void CListCtrlEx::SetColumnVisible(int nCol, BOOL bVisible)
|
||||
if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见
|
||||
m_Columns[nCol].Visible = bVisible;
|
||||
AdjustColumnWidths();
|
||||
// 列数变化后完整重绘(含背景擦除),否则最后一列右侧会残留旧列残影。
|
||||
ScopedEraseBkgnd scope(*this);
|
||||
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +233,11 @@ void CListCtrlEx::ToggleColumnVisibility(int nColumn)
|
||||
|
||||
// 重新调整列宽
|
||||
AdjustColumnWidths();
|
||||
|
||||
// 列数变化后完整重绘(含背景擦除),否则最后一列(TLS 等)右侧会残留旧列残影。
|
||||
// 与 OnSize 中 needRefresh 分支相同的处理:临时放行背景擦除 + 全量 RedrawWindow。
|
||||
ScopedEraseBkgnd scope(*this);
|
||||
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
|
||||
}
|
||||
|
||||
void CListCtrlEx::LoadColumnVisibility()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,11 +89,29 @@ protected:
|
||||
{
|
||||
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();
|
||||
TranslateDialog(this);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,9 @@ typedef struct FeatureFlags {
|
||||
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
|
||||
|
||||
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
|
||||
#define MF_TLS_CERTIFICATE (1ULL << 44) // HIDE_MENU_TLS_CERTIFICATE
|
||||
|
||||
// [44-63] Reserved
|
||||
// [45-63] Reserved
|
||||
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -223,6 +223,27 @@ int GetIconIndex(LPCTSTR lpFileName, DWORD dwFileAttributes)
|
||||
return index;
|
||||
}
|
||||
|
||||
// 远程文件名/路径编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA)=GBK/936,
|
||||
// LNX/MAC 文件系统天然 UTF-8(同 McpServer.cpp::BuildListFiles)。这里用 LVM_SETITEMTEXTW
|
||||
// 直接写宽字符串,绕开 ANSI→CP_ACP 回转,即使服务端运行在非中文 ACP 上中文名也能正常显示。
|
||||
static void SetFileTextItem(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
|
||||
{
|
||||
std::wstring w;
|
||||
if (sz && sz[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 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);
|
||||
}
|
||||
|
||||
BOOL CFileManagerDlg::OnInitDialog()
|
||||
{
|
||||
__super::OnInitDialog();
|
||||
@@ -1104,6 +1125,9 @@ void CFileManagerDlg::OnDblclkListRemote(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
|
||||
void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
|
||||
{
|
||||
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
|
||||
|
||||
// 重新设置ImageList
|
||||
SHFILEINFO sfi = {};
|
||||
HIMAGELIST hImageListLarge = (HIMAGELIST)SHGetFileInfo(NULL, 0, &sfi,sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_LARGEICON);
|
||||
@@ -1151,7 +1175,8 @@ void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
|
||||
pszFileName = ++pList;
|
||||
|
||||
if (bIsInsert) {
|
||||
nItem = m_list_remote.InsertItem(nItemIndex++, pszFileName, GetIconIndex(pszFileName, nType));
|
||||
nItem = m_list_remote.InsertItem(nItemIndex++, "", GetIconIndex(pszFileName, nType));
|
||||
SetFileTextItem(m_list_remote, nItem, 0, pszFileName, cp);
|
||||
m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
|
||||
SHFILEINFO sfi = {};
|
||||
SHGetFileInfo(pszFileName, FILE_ATTRIBUTE_NORMAL | nType, &sfi,sizeof(SHFILEINFO), SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES);
|
||||
@@ -1184,6 +1209,9 @@ void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
|
||||
|
||||
void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLen)
|
||||
{
|
||||
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
|
||||
|
||||
// 第一批结果到达时,初始化搜索列表
|
||||
if (!m_bSearching) {
|
||||
m_bSearching = true;
|
||||
@@ -1241,7 +1269,8 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLe
|
||||
}
|
||||
}
|
||||
|
||||
int nItem = m_list_remote.InsertItem(m_nSearchResultCount, csFileName, nIcon);
|
||||
int nItem = m_list_remote.InsertItem(m_nSearchResultCount, "", nIcon);
|
||||
SetFileTextItem(m_list_remote, nItem, 0, csFileName, cp);
|
||||
m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
|
||||
|
||||
pList += lstrlen(pszFullPath) + 1;
|
||||
@@ -1261,7 +1290,7 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLe
|
||||
m_list_remote.SetItemText(nItem, 2, time.FormatL("%Y-%m-%d %H:%M"));
|
||||
|
||||
// 路径
|
||||
m_list_remote.SetItemText(nItem, 3, csDir);
|
||||
SetFileTextItem(m_list_remote, nItem, 3, csDir, cp);
|
||||
|
||||
pList += 16;
|
||||
m_nSearchResultCount++;
|
||||
@@ -3329,7 +3358,7 @@ void CFileManagerDlg::OnTransferV2ToRemote()
|
||||
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
|
||||
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
|
||||
} else {
|
||||
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID);
|
||||
ShowMessage(_TRF("找不到主连接: clientID %llu"), clientID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
|
||||
CString os = ctx->GetClientData(ONLINELIST_OS);
|
||||
device["os"] = AnsiToUtf8(os);
|
||||
|
||||
// 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
|
||||
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
|
||||
device["location"] = AnsiToUtf8(location);
|
||||
device["location"] = std::string(location.GetString());
|
||||
|
||||
CString rtt = ctx->GetClientData(ONLINELIST_PING);
|
||||
device["rtt"] = AnsiToUtf8(rtt);
|
||||
@@ -113,5 +114,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
|
||||
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
|
||||
|
||||
// 登录时上报的 TLS 传输标志(RES_TLS=="1" → "Y")。老客户端不上报,取值为 "N"。
|
||||
// 注意这是登录凭据而不是实时探测:连接建立后若配置发生变化,需要客户端重连才会更新。
|
||||
device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");
|
||||
|
||||
return device;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ void IOCPKCPServer::WorkerThread()
|
||||
}
|
||||
if (n > 0&& m_notify) {
|
||||
memcpy(ctx->szBuffer, recvbuf, n);
|
||||
BOOL ret = ParseReceivedData(ctx, n, m_notify);
|
||||
BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, n, m_notify);
|
||||
}
|
||||
} while (n>0);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "2015Remote.h"
|
||||
#include "common/IPWhitelist.h"
|
||||
#include "common/IPBlacklist.h"
|
||||
#include "ServerTlsSession.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <ws2tcpip.h>
|
||||
@@ -483,6 +484,11 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
||||
m_nPort = uPort;
|
||||
m_NotifyProc = NotifyProc;
|
||||
m_OfflineProc = OffProc;
|
||||
|
||||
// 单向 TLS:启动时加载服务端身份(证书+私钥)。加载失败时 IsLoadError()==true,
|
||||
// 明文客户端不受影响,TLS ClientHello 会被识别并 fail-closed 关闭(见设计文档 §5.2)。
|
||||
ServerTlsIdentity::Load(THIS_CFG);
|
||||
|
||||
// manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。
|
||||
// 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。
|
||||
// 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。
|
||||
@@ -793,16 +799,16 @@ BOOL IOCPServer::OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTr
|
||||
}
|
||||
|
||||
// May be this function should be a member of `CONTEXT_OBJECT`.
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z)
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z)
|
||||
{
|
||||
AUTO_TICK(50, ContextObject->GetPeerName());
|
||||
BOOL ret = 1;
|
||||
try {
|
||||
if (dwTrans == 0) { //对方关闭了套接字
|
||||
if (dataLen == 0) { //对方关闭了套接字
|
||||
return FALSE;
|
||||
}
|
||||
//将接收到的数据拷贝到我们自己的内存中wsabuff 8192
|
||||
ContextObject->InCompressedBuffer.WriteBuffer((PBYTE)ContextObject->szBuffer,dwTrans);
|
||||
ContextObject->InCompressedBuffer.WriteBuffer(data, dataLen);
|
||||
//查看数据包的完整性
|
||||
while (true) {
|
||||
PR pr = ContextObject->Parse(ContextObject->InCompressedBuffer);
|
||||
@@ -889,18 +895,178 @@ BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyP
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z)
|
||||
// 握手线程参数:把嗅探阶段已收到的 ClientHello 字节与连接上下文一起移交。
|
||||
struct TlsHandshakeParam {
|
||||
IOCPServer* server;
|
||||
CONTEXT_OBJECT* ctx;
|
||||
std::vector<BYTE> initial; // 嗅探阶段已攒下的首批字节(≥6,可能只是 ClientHello 分片)
|
||||
};
|
||||
|
||||
// 方案 A:专用阻塞握手线程。占线期间独占 socket(阻塞 recv/send,无未决 overlapped I/O),
|
||||
// 完成后把连接交还给 IOCP(PostRecv)。线程持有 ctx 的一枚 IoRefCount,结束时释放。
|
||||
DWORD WINAPI IOCPServer::TlsHandshakeThreadProc(LPVOID lParam)
|
||||
{
|
||||
if (FALSE == ParseReceivedData(ContextObject, dwTrans, m_NotifyProc, ctx, z)) {
|
||||
// 先减少引用计数,再调用 RemoveStaleContext (#215)
|
||||
// RemoveStaleContext 完成后对象会被移到空闲池,不能再访问
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2; // 特殊返回值:告诉 HandleIO 不要再减少引用计数
|
||||
TlsHandshakeParam* p = (TlsHandshakeParam*)lParam;
|
||||
IOCPServer* This = p->server;
|
||||
CONTEXT_OBJECT* ctx = p->ctx;
|
||||
std::vector<BYTE> initial = std::move(p->initial);
|
||||
delete p;
|
||||
|
||||
// 若上下文已被移除(服务端关停竞态),直接释放引用退出。
|
||||
if (ctx->IsRemoved.load(std::memory_order_acquire)) {
|
||||
ctx->IoRefCount.fetch_sub(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PostRecv(ContextObject); //投递新的接收数据的请求
|
||||
// 握手期间做同步 recv/send。注意:监听套接字经 WSAEventSelect 置为非阻塞后,accept 出的
|
||||
// 子套接字继承非阻塞态,FIONBIO=0 不一定能切回阻塞;ServerTlsSession::Handshake 内部已用
|
||||
// select 兜底 WSAEWOULDBLOCK,故这里仍设 FIONBIO=0 只是「尽量阻塞」的优化。
|
||||
u_long nb = 0;
|
||||
if (ioctlsocket(ctx->sClientSocket, FIONBIO, &nb) == SOCKET_ERROR)
|
||||
Mprintf("[TLS] handshake: ioctlsocket(FIONBIO=0) failed err=%d\n", WSAGetLastError());
|
||||
int tmo = 15000; // 15s 握手超时
|
||||
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));
|
||||
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tmo, sizeof(tmo));
|
||||
|
||||
bool ok = (ctx->tls != NULL) &&
|
||||
ctx->tls->Handshake(ctx->sClientSocket, initial.data(), (int)initial.size());
|
||||
|
||||
if (ok) {
|
||||
ctx->tlsState.store(TLS_Ready, std::memory_order_release);
|
||||
}
|
||||
|
||||
// 先释放握手线程持有的引用,再触发任何可能 RemoveStaleContext 的动作(#215 顺序)。
|
||||
ctx->IoRefCount.fetch_sub(1);
|
||||
|
||||
if (ok) {
|
||||
This->PostRecv(ctx); // 交还 IOCP 接收
|
||||
} else {
|
||||
Mprintf("[TLS] handshake failed for %s (fail-closed)\n", ctx->GetPeerName().c_str());
|
||||
This->RemoveStaleContext(ctx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 判定为 TLS 后:创建会话、预支一枚 IoRefCount 给握手线程、启动握手线程。
|
||||
BOOL IOCPServer::StartTlsHandshake(CONTEXT_OBJECT* ContextObject)
|
||||
{
|
||||
if (!ServerTlsIdentity::IsReady()) {
|
||||
// 服务端身份不可用(未启用 TLS、或证书/私钥加载失败):fail-closed 关闭该连接。
|
||||
Mprintf("[TLS] client %s sent ClientHello but server identity not ready "
|
||||
"(TLS disabled or cert/key load failed); closing (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ContextObject->tls = new ServerTlsSession();
|
||||
TlsHandshakeParam* p = new TlsHandshakeParam;
|
||||
p->server = this;
|
||||
p->ctx = ContextObject;
|
||||
p->initial = std::move(ContextObject->tlsPreClassify);
|
||||
ContextObject->tlsPreClassify.clear();
|
||||
|
||||
// 预支引用:握手线程结束时释放。此引用保证握手期间对象不被回收。
|
||||
ContextObject->IoRefCount.fetch_add(1);
|
||||
|
||||
HANDLE h = CreateThread(NULL, 0, TlsHandshakeThreadProc, p, 0, NULL);
|
||||
if (h == NULL) {
|
||||
// 启动失败:回退预支引用,清理会话,按失败关闭。
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
delete ContextObject->tls;
|
||||
ContextObject->tls = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
CloseHandle(h);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z)
|
||||
{
|
||||
int st = ContextObject->tlsState.load(std::memory_order_acquire);
|
||||
|
||||
// ---- 未分类:按 ClientHello 头嗅探明文 / TLS(见设计文档 §5.2)----
|
||||
if (st == TLS_Unclassified) {
|
||||
if (dwTrans == 0) { // 首个包即对端关闭
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
ContextObject->tlsPreClassify.insert(ContextObject->tlsPreClassify.end(),
|
||||
(BYTE*)ContextObject->szBuffer,
|
||||
(BYTE*)ContextObject->szBuffer + dwTrans);
|
||||
if (ContextObject->tlsPreClassify.size() < 6) {
|
||||
PostRecv(ContextObject); // 头还没攒够,继续收
|
||||
return TRUE;
|
||||
}
|
||||
const BYTE* h = ContextObject->tlsPreClassify.data();
|
||||
bool isTls = (h[0] == 0x16 && h[1] == 0x03 && h[5] == 0x01);
|
||||
if (isTls) {
|
||||
ContextObject->tlsState.store(TLS_Handshaking, std::memory_order_release);
|
||||
if (!StartTlsHandshake(ContextObject)) {
|
||||
Mprintf("[TLS] StartTlsHandshake failed for %s (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
return TRUE; // 不 PostRecv;握手线程完成后交还接收
|
||||
}
|
||||
|
||||
// 明文:把嗅探阶段攒下的字节回放给现有解析路径,然后清空缓冲。
|
||||
ContextObject->tlsState.store(TLS_Plain, std::memory_order_release);
|
||||
std::vector<BYTE> replay = std::move(ContextObject->tlsPreClassify);
|
||||
ContextObject->tlsPreClassify.clear();
|
||||
if (FALSE == ParseReceivedData(ContextObject, replay.data(), (DWORD)replay.size(), m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// ---- 已分类 ----
|
||||
if (st == TLS_Ready) {
|
||||
if (dwTrans == 0) { // 明文 FIN:TLS 连接对端关闭
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
std::vector<BYTE> plain;
|
||||
if (ContextObject->tls->Decrypt((const BYTE*)ContextObject->szBuffer, dwTrans, plain) < 0) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
if (plain.empty()) {
|
||||
PostRecv(ContextObject); // 本轮无完整明文(TLS 记录分片/握手告警),继续收
|
||||
return TRUE;
|
||||
}
|
||||
if (FALSE == ParseReceivedData(ContextObject, plain.data(), (DWORD)plain.size(), m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// TLS_Handshaking:握手线程独占 socket,正常不应出现 IORead;若出现属异常,fail-closed。
|
||||
if (st == TLS_Handshaking) {
|
||||
Mprintf("[TLS] unexpected IORead during handshake for %s (closing)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
|
||||
// TLS_Plain:明文直通(原路径)。
|
||||
if (FALSE == ParseReceivedData(ContextObject, (PBYTE)ContextObject->szBuffer, dwTrans, m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -967,15 +1133,49 @@ BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOr
|
||||
|
||||
BOOL IOCPServer::OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength)
|
||||
{
|
||||
// TLS 下 OutCompressedBuffer 可能还残留上一轮尚未发完的密文(IOCP 线程异步 WSASend 中),
|
||||
// 新明文由 WriteContextData 追加在尾部。若把整个缓冲区重新加密,会把旧密文二次加密,
|
||||
// 客户端解密后得到的是「旧 TLS 记录」→ Unknown header 17 03 03。故只加密本次新追加的尾部。
|
||||
bool tlsReady = (ContextObject->tlsState.load(std::memory_order_acquire) == TLS_Ready);
|
||||
ULONG oldLen = tlsReady ? ContextObject->OutCompressedBuffer.GetBufferLength() : 0;
|
||||
|
||||
if (WriteContextData(ContextObject, szBuffer, ulOriginalLength, ContextObject->Zcctx)) {
|
||||
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite);
|
||||
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol);
|
||||
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败
|
||||
int a = GetLastError();
|
||||
Mprintf("!!! OnClientPreSending 投递消息失败\n");
|
||||
RemoveStaleContext(ContextObject);
|
||||
SAFE_DELETE(OverlappedPlus);
|
||||
return FALSE;
|
||||
// 单向 TLS:只加密新追加的明文尾部,产出密文后与前缀旧密文拼接,使缓冲区始终保持
|
||||
// 一串合法 TLS 记录;之后 IOWrite / OnClientPostSending 完全不变(见设计文档 §5.5)。
|
||||
if (tlsReady) {
|
||||
ULONG totalLen = ContextObject->OutCompressedBuffer.GetBufferLength();
|
||||
ULONG plainLen = totalLen - oldLen;
|
||||
if (plainLen > 0) {
|
||||
std::vector<BYTE> wire;
|
||||
if (ContextObject->tls->Encrypt((const BYTE*)ContextObject->OutCompressedBuffer.GetBuffer(oldLen),
|
||||
(int)plainLen, wire) != (int)plainLen) {
|
||||
Mprintf("[TLS] Encrypt failed for %s (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
return FALSE; // 加密失败:丢弃本次发送
|
||||
}
|
||||
// 截断尾部明文,再追加密文。用 TruncateBuffer 而非 ClearBuffer:ClearBuffer 会
|
||||
// DeAllocateBuffer(可能 VirtualFree 正在被异步 WSASend 引用的底层内存)。
|
||||
ContextObject->OutCompressedBuffer.TruncateBuffer(oldLen);
|
||||
ContextObject->OutCompressedBuffer.WriteBuffer(wire.data(), (DWORD)wire.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 仅当没有发送正在进行时才投递 IOWrite 唤醒发送。若已有 WSASend 在途,其完成回调
|
||||
// (OnClientPostSending)会看到排空后仍残留的新数据并继续发送,无需再投递;否则会对
|
||||
// 同一缓冲并发多个 WSASend,读到同一段可变内存,产出重复/错位密文(客户端 DecryptMessage
|
||||
// 失败,表现为 recv -1、GetLastError=6001)。
|
||||
if (!ContextObject->bSendPending) {
|
||||
ContextObject->bSendPending = true;
|
||||
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite);
|
||||
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol);
|
||||
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败
|
||||
int a = GetLastError();
|
||||
Mprintf("!!! OnClientPreSending 投递消息失败\n");
|
||||
ContextObject->bSendPending = false;
|
||||
RemoveStaleContext(ContextObject);
|
||||
SAFE_DELETE(OverlappedPlus);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -990,6 +1190,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
||||
|
||||
ContextObject->OutCompressedBuffer.RemoveCompletedBuffer(ulCompletedLength); //将完成的数据从数据结构中去除
|
||||
if (ContextObject->OutCompressedBuffer.GetBufferLength() == 0) {
|
||||
ContextObject->bSendPending = false; // 缓冲排空,允许下一次 OnClientPreSending 投递
|
||||
ContextObject->OutCompressedBuffer.ClearBuffer();
|
||||
return true; //走到这里说明我们的数据真正完全发送
|
||||
} else {
|
||||
@@ -1001,6 +1202,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
||||
NULL, ulFlags,&OverlappedPlus->m_ol, NULL);
|
||||
if ( iOk == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) {
|
||||
// 先减少引用计数,再调用 RemoveStaleContext (#215)
|
||||
ContextObject->bSendPending = false;
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
if (RemoveStaleContext(ContextObject))
|
||||
Mprintf("!!! OnClientPostSending 投递消息失败: %d\n", WSAGetLastError());
|
||||
|
||||
@@ -91,6 +91,7 @@ private:
|
||||
static DWORD WINAPI ListenThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI RttPollThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI TlsHandshakeThreadProc(LPVOID lParam);
|
||||
|
||||
BOOL InitializeIOCP(VOID);
|
||||
VOID OnAccept();
|
||||
@@ -101,6 +102,7 @@ private:
|
||||
BOOL HandleIO(IOType PacketFlags, PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream *z);
|
||||
BOOL OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTrans);
|
||||
BOOL OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z);
|
||||
BOOL StartTlsHandshake(CONTEXT_OBJECT* ContextObject);
|
||||
BOOL OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength);
|
||||
BOOL OnClientPostSending(CONTEXT_OBJECT* ContextObject, ULONG ulCompressedLength);
|
||||
int AddWorkThread(int n)
|
||||
@@ -257,6 +259,8 @@ public:
|
||||
|
||||
typedef CDialogBase DialogBase;
|
||||
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL);
|
||||
// 解析一段已解密/直通的接收字节。data/dataLen 为数据源(TLS 连接传解密后的明文,
|
||||
// 明文连接传 szBuffer 原始字节);dataLen==0 视为对端关闭。
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL);
|
||||
|
||||
BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength, ZSTD_CCtx *ctx=NULL, z_stream* z = NULL);
|
||||
@@ -107,7 +107,7 @@ void IOCPUDPServer::WorkerThread()
|
||||
|
||||
IO_CONTEXT* ioCtx = CONTAINING_RECORD(pOverlapped, IO_CONTEXT, ol);
|
||||
CONTEXT_UDP* ctx = ioCtx->pContext;
|
||||
BOOL ret = ParseReceivedData(ctx, bytes, m_notify);
|
||||
BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, bytes, m_notify);
|
||||
if (999 != ret)
|
||||
ctx->Destroy();
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ BOOL CKeyBoardDlg::OnInitDialog()
|
||||
// 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。
|
||||
// -----------------------------------------------------------------
|
||||
RebuildEdit(m_edit);
|
||||
// 剪切板显示框同样重建为 Unicode 控件,ShowClipboardText 全程走 W 版消息,
|
||||
// 否则 MBCS 下 SetWindowTextA 会在 A<->W 边界按 CP_ACP 转码,中文丢失。
|
||||
RebuildEdit(m_EditClipText);
|
||||
|
||||
m_edit.SetLimitText(MAXDWORD); // 设置最大长度
|
||||
auto rule = utf8_to_ansi((char*)m_TextRule.param);
|
||||
@@ -171,9 +174,19 @@ void CKeyBoardDlg::OnReceiveComplete()
|
||||
LRESULT CKeyBoardDlg::ShowClipboardText(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
char* buf = (char*)wParam;
|
||||
std::string text = utf8_to_ansi(buf);
|
||||
// 剪切板文本是 UTF-8(client/my_clip.h 的 clip::get_text 从 CF_UNICODETEXT 转出)。
|
||||
// 直接转宽字符串,用 W 版写入重建后的 Unicode 控件,绕开 utf8_to_ansi 的 CP_ACP
|
||||
// 回转——非中文系统(德语/英语)上中文也能正常显示(与 m_edit 同法)。
|
||||
std::wstring w;
|
||||
if (buf && buf[0]) {
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(CP_UTF8, 0, buf, -1, &w[0], wlen);
|
||||
}
|
||||
}
|
||||
SAFE_DELETE_ARRAY(buf);
|
||||
m_EditClipText.SetWindowTextA(text.c_str());
|
||||
::SetWindowTextW(m_EditClipText.GetSafeHwnd(), w.c_str());
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -723,11 +723,11 @@ protected:
|
||||
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
|
||||
AppendWord(0); // 菜单
|
||||
AppendWord(0); // 窗口类
|
||||
AppendString(_T("切换语言 / Switch Language"));
|
||||
AppendString(_T("切换语言"));
|
||||
AlignToDword();
|
||||
|
||||
// 静态文本
|
||||
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
|
||||
AddControl(0x0082, 15, 15, 50, 12, 1002,
|
||||
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
|
||||
|
||||
// ComboBox
|
||||
@@ -796,8 +796,17 @@ protected:
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
// 翻译对话框控件(标题、标签、按钮)
|
||||
TranslateDialog(this);
|
||||
// 模板为动态构造的 Unicode 模板,MBCS 下被 ANSI 创建路径误读,标题/标签/按钮文字会乱码,
|
||||
// 故在此显式设置(走线程 locale GBK,与其它对话框同法),不再对已乱码文本调用 TranslateDialog。
|
||||
// 标题含中文:非中文系统上 ANSI 消息按系统 ACP 解码会乱码,用 _TR 取翻译(中文→切换语言,
|
||||
// 英语→Switch Language)后,显式 GBK(936)→Unicode 转换再用 Unicode 消息设置(与工具栏同法)。
|
||||
CString strTitle = _TR("切换语言");
|
||||
WCHAR wTitle[64] = { 0 };
|
||||
MultiByteToWideChar(936, 0, strTitle, -1, wTitle, 64);
|
||||
::SetWindowTextW(m_hWnd, wTitle);
|
||||
SetDlgItemText(1002, _TR("语言:"));
|
||||
SetDlgItemText(IDOK, _TR("确定"));
|
||||
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||
|
||||
m_comboLang.SubclassDlgItem(1001, this);
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#include "stdafx.h"
|
||||
#include "McpInternal.h"
|
||||
#include "context.h" // context 接口(GetClientID/IsLogin)
|
||||
#include "2015RemoteDlg.h" // CMy2015RemoteDlg 成员(m_HostList/m_cs)+ WM_SHOWERRORMSG
|
||||
#include "LangManager.h" // _TR
|
||||
|
||||
#ifndef _WIN64
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
|
||||
#endif
|
||||
#else
|
||||
#ifdef _DEBUG
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
|
||||
#else
|
||||
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace McpInternal {
|
||||
|
||||
std::string JsonToString(const Json::Value& v) {
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
resp["result"] = result;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
Json::Value err(Json::objectValue);
|
||||
err["code"] = code;
|
||||
err["message"] = msg;
|
||||
resp["error"] = err;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(const std::string& s) {
|
||||
std::string r = s;
|
||||
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
return r;
|
||||
}
|
||||
|
||||
bool ContainsCI(const std::string& haystack, const std::string& needle) {
|
||||
if (needle.empty()) return true;
|
||||
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
|
||||
}
|
||||
|
||||
std::string JsonStrField(const Json::Value& v, const char* key) {
|
||||
if (v.isObject() && v.isMember(key) && v[key].isString())
|
||||
return v[key].asString();
|
||||
return "";
|
||||
}
|
||||
|
||||
bool IsDigits(const std::string& s) {
|
||||
if (s.empty()) return false;
|
||||
for (char c : s) if (c < '0' || c > '9') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GetStringArg(const Json::Value& args, const char* key) {
|
||||
return JsonStrField(args, key);
|
||||
}
|
||||
|
||||
bool GetIntArg(const Json::Value& args, const char* key, int& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
const Json::Value& v = args[key];
|
||||
if (v.isNumeric()) { out = v.asInt(); return true; }
|
||||
if (v.isString()) {
|
||||
const std::string s = v.asString();
|
||||
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
|
||||
out = atoi(s.c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReadUInt64(const Json::Value& v, uint64_t& out) {
|
||||
if (v.isNumeric()) { out = v.asUInt64(); return true; }
|
||||
if (v.isString()) {
|
||||
std::string s = v.asString();
|
||||
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
return ReadUInt64(args[key], out);
|
||||
}
|
||||
|
||||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
|
||||
std::string sid = GetStringArg(args, "id");
|
||||
if (sid.empty()) {
|
||||
err = "Missing required parameter: id";
|
||||
return false;
|
||||
}
|
||||
if (!IsDigits(sid)) {
|
||||
err = "Invalid id: expected a decimal host id string";
|
||||
return false;
|
||||
}
|
||||
out = strtoull(sid.c_str(), nullptr, 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
|
||||
if (!parent) return nullptr;
|
||||
context* found = nullptr;
|
||||
EnterCriticalSection(&parent->m_cs);
|
||||
for (context* ctx : parent->m_HostList) {
|
||||
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
|
||||
found = ctx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&parent->m_cs);
|
||||
return found;
|
||||
}
|
||||
|
||||
size_t BoundedStrlen(const char* p, size_t avail) {
|
||||
size_t n = 0;
|
||||
while (n < avail && p[n]) ++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
std::string ToUtf8(const char* s, UINT cp) {
|
||||
if (!s || !*s) return "";
|
||||
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||||
if (wlen <= 0) return "";
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 0) return "";
|
||||
std::string out(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||||
if (utf8.empty()) return "";
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||
if (wlen <= 0) return utf8;
|
||||
std::wstring w(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
|
||||
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
|
||||
if (alen <= 0) return utf8;
|
||||
std::string out(alen, '\0');
|
||||
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP管理操作")));
|
||||
}
|
||||
|
||||
} // namespace McpInternal
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
// MCP 工具实现共享的内部辅助函数。
|
||||
//
|
||||
// 这些函数原先都定义在 McpServer.cpp 的匿名命名空间里;主机管理 / 连接管理
|
||||
// 工具族(McpMachine.cpp)需要复用同一套 JSON、host id 解析与编码转换逻辑,
|
||||
// 故抽出到独立编译单元,由 McpServer.cpp 与 McpMachine.cpp 共同引用。
|
||||
//
|
||||
// 约定:函数语义与实现与抽取前完全一致(McpServer.cpp 通过 using-directive
|
||||
// 引入,调用点不变),不做行为改动。
|
||||
|
||||
#include <Windows.h>
|
||||
#include "jsoncpp/json.h"
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
class context;
|
||||
|
||||
namespace McpInternal {
|
||||
|
||||
// Json::Value → 紧凑 JSON 字符串
|
||||
std::string JsonToString(const Json::Value& v);
|
||||
|
||||
// JSON-RPC 2.0 成功 / 错误响应
|
||||
std::string BuildResult(const Json::Value& id, const Json::Value& result);
|
||||
std::string BuildError(const Json::Value& id, int code, const std::string& msg);
|
||||
|
||||
// 仅 ASCII 小写化;不区分大小写的子串匹配
|
||||
std::string ToLowerAscii(const std::string& s);
|
||||
bool ContainsCI(const std::string& haystack, const std::string& needle);
|
||||
|
||||
// 入参读取
|
||||
std::string JsonStrField(const Json::Value& v, const char* key);
|
||||
bool IsDigits(const std::string& s);
|
||||
std::string GetStringArg(const Json::Value& args, const char* key);
|
||||
bool GetIntArg(const Json::Value& args, const char* key, int& out);
|
||||
bool ReadUInt64(const Json::Value& v, uint64_t& out);
|
||||
bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out);
|
||||
|
||||
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false 并写 err。
|
||||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err);
|
||||
|
||||
// 在 m_HostList 中按 clientID 找在线主 context(锁内遍历 m_cs)。
|
||||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id);
|
||||
|
||||
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||||
size_t BoundedStrlen(const char* p, size_t avail);
|
||||
|
||||
// 编码转换:任意代码页 → UTF-8(输出 JSON 用);UTF-8 → 目标代码页(下发客户端用)。
|
||||
std::string ToUtf8(const char* s, UINT cp);
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp);
|
||||
|
||||
// 写操作审计:PostMessageA(WM_SHOWERRORMSG) 落到服务端消息/审计日志(不可关闭)。
|
||||
void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail);
|
||||
|
||||
} // namespace McpInternal
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
// 主机管理(10 个标签页)与连接管理(连接列表 / 强制断开)MCP 工具族。
|
||||
//
|
||||
// 协议(详见 docs/Mcp_Machine_Design.md):
|
||||
// 主机管理:主连接 COMMAND_MACHINE_MANAGE(162) → 客户端 MachineManager 子连接回
|
||||
// TOKEN_SYSINFOLIST(61) → 发 1 字节 tab(0..9) → 回 TOKEN_MACHINE_*(31..42)。
|
||||
// 连接管理:主连接 COMMAND_CONNECTION(253) → CConnectionsManager 子连接回
|
||||
// TOKEN_CONNECTION_LIST(255) → 发 254+原始 110 字节记录强制断开。
|
||||
//
|
||||
// 会话状态由 CMcpServer 持有(与 m_Pending 共用单飞行表),本模块只负责工具 schema、
|
||||
// 参数校验、组包与解析。
|
||||
|
||||
#include "jsoncpp/json.h"
|
||||
#include <string>
|
||||
|
||||
class CMy2015RemoteDlg;
|
||||
|
||||
namespace McpMachine {
|
||||
|
||||
// 是否由本模块处理(AppendTools 与 Call 必须保持一致)。
|
||||
bool Handles(const std::string& toolName);
|
||||
|
||||
// 按当前开关(McpMachine / McpMachineDangerous / McpReadonly)追加可见工具。
|
||||
void AppendTools(Json::Value& tools);
|
||||
|
||||
// 执行工具;返回完整 JSON-RPC 响应字符串(成功 = BuildResult,失败 = BuildError)。
|
||||
std::string Call(const Json::Value& id, const std::string& toolName,
|
||||
const Json::Value& args, CMy2015RemoteDlg* parent);
|
||||
|
||||
} // namespace McpMachine
|
||||
+30
-176
@@ -8,6 +8,8 @@
|
||||
#include "Server.h" // CONTEXT_OBJECT 定义(GetScreenContext 返回 CONTEXT_OBJECT* → context* 上转型)
|
||||
#include "LangManager.h" // _TR(审计日志标题语言映射)
|
||||
#include "common/file_upload.h" // V2 文件传输(RecvFileChunkV2 / HandleFileCompleteV2 / 包结构)
|
||||
#include "McpInternal.h" // 共享助手(McpMachine.cpp 也使用)
|
||||
#include "McpMachine.h" // 主机管理 / 连接管理工具族
|
||||
|
||||
#include <sstream>
|
||||
|
||||
@@ -30,39 +32,15 @@
|
||||
std::string GetPwdHash();
|
||||
std::string GetHMAC(int offset);
|
||||
|
||||
// 共享助手(JsonToString/BuildResult/ParseHostIdArg/FindMainContext/ToUtf8/ToAnsi/...)
|
||||
// 已抽到 McpInternal.cpp;此处引入后调用点保持原样。
|
||||
using namespace McpInternal;
|
||||
|
||||
namespace {
|
||||
|
||||
// P2b 工具等待响应的超时(ms)。MCP 一次性请求:等待子连接回传进程/窗口列表。
|
||||
static const int kMcpToolTimeoutMs = 20000;
|
||||
|
||||
// Json::Value → 紧凑 JSON 字符串
|
||||
std::string JsonToString(const Json::Value& v) {
|
||||
Json::StreamWriterBuilder b;
|
||||
b["indentation"] = "";
|
||||
return Json::writeString(b, v);
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 成功响应
|
||||
std::string BuildResult(const Json::Value& id, const Json::Value& result) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
resp["result"] = result;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 错误响应
|
||||
std::string BuildError(const Json::Value& id, int code, const std::string& msg) {
|
||||
Json::Value resp(Json::objectValue);
|
||||
resp["jsonrpc"] = "2.0";
|
||||
resp["id"] = id;
|
||||
Json::Value err(Json::objectValue);
|
||||
err["code"] = code;
|
||||
err["message"] = msg;
|
||||
resp["error"] = err;
|
||||
return JsonToString(resp);
|
||||
}
|
||||
|
||||
// initialize 握手(MCP 规范:protocolVersion + capabilities + serverInfo)
|
||||
std::string BuildInitializeResult(const Json::Value& id) {
|
||||
Json::Value result(Json::objectValue);
|
||||
@@ -84,33 +62,6 @@ std::string BuildPingResult(const Json::Value& id) {
|
||||
|
||||
// ========== P2a 通用辅助 ==========
|
||||
|
||||
// 小写化(仅 ASCII,UTF-8 多字节原样保留):用于不区分大小写的子串匹配
|
||||
std::string ToLowerAscii(const std::string& s) {
|
||||
std::string r = s;
|
||||
for (char& c : r) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
return r;
|
||||
}
|
||||
|
||||
// 不区分大小写的子串匹配
|
||||
bool ContainsCI(const std::string& haystack, const std::string& needle) {
|
||||
if (needle.empty()) return true;
|
||||
return ToLowerAscii(haystack).find(ToLowerAscii(needle)) != std::string::npos;
|
||||
}
|
||||
|
||||
// 取对象的字符串字段,缺失/非字符串返回 ""
|
||||
std::string JsonStrField(const Json::Value& v, const char* key) {
|
||||
if (v.isObject() && v.isMember(key) && v[key].isString())
|
||||
return v[key].asString();
|
||||
return "";
|
||||
}
|
||||
|
||||
// 是否纯数字(host id 为 uint64 十进制字符串)
|
||||
bool IsDigits(const std::string& s) {
|
||||
if (s.empty()) return false;
|
||||
for (char c : s) if (c < '0' || c > '9') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 读取 tools/call 的入参(MCP 规范:params.arguments 为工具入参对象)
|
||||
Json::Value GetCallArguments(const Json::Value& params) {
|
||||
if (params.isObject() && params.isMember("arguments") && params["arguments"].isObject())
|
||||
@@ -118,85 +69,8 @@ Json::Value GetCallArguments(const Json::Value& params) {
|
||||
return Json::Value(Json::objectValue);
|
||||
}
|
||||
|
||||
// 读取可选字符串入参,缺失返回 ""
|
||||
std::string GetStringArg(const Json::Value& args, const char* key) {
|
||||
return JsonStrField(args, key);
|
||||
}
|
||||
|
||||
// 读取可选整数入参(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
|
||||
// 定义见文件后半段(随 P3 终端会话辅助一起),此处前置声明供早于定义的 exec_command /
|
||||
// terminal_exec 等工具复用。
|
||||
static bool GetIntArg(const Json::Value& args, const char* key, int& out);
|
||||
|
||||
// ========== P2b 辅助 ==========
|
||||
|
||||
// 解析 id 入参(必填、纯数字)为 uint64;非法返回 false。
|
||||
bool ParseHostIdArg(const Json::Value& args, uint64_t& out, std::string& err) {
|
||||
std::string sid = GetStringArg(args, "id");
|
||||
if (sid.empty()) {
|
||||
err = "Missing required parameter: id";
|
||||
return false;
|
||||
}
|
||||
if (!IsDigits(sid)) {
|
||||
err = "Invalid id: expected a decimal host id string";
|
||||
return false;
|
||||
}
|
||||
out = strtoull(sid.c_str(), nullptr, 10);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 在 m_HostList 中按 clientID 找在线主 context(复刻 CollectOnlineHosts 的锁内遍历)。
|
||||
context* FindMainContext(CMy2015RemoteDlg* parent, uint64_t id) {
|
||||
if (!parent) return nullptr;
|
||||
context* found = nullptr;
|
||||
EnterCriticalSection(&parent->m_cs);
|
||||
for (context* ctx : parent->m_HostList) {
|
||||
if (ctx && ctx->GetClientID() == id && ctx->IsLogin()) {
|
||||
found = ctx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&parent->m_cs);
|
||||
return found;
|
||||
}
|
||||
|
||||
// 有界 strlen:p 最多可读 avail 字节,返回 n < avail 表示遇到 '\0'。
|
||||
size_t BoundedStrlen(const char* p, size_t avail) {
|
||||
size_t n = 0;
|
||||
while (n < avail && p[n]) ++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// 从源编码 cp 转为 UTF-8(输出到 JSON)。空/失败返回 ""。
|
||||
std::string ToUtf8(const char* s, UINT cp) {
|
||||
if (!s || !*s) return "";
|
||||
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
|
||||
if (wlen <= 0) return "";
|
||||
std::wstring w(wlen - 1, L'\0');
|
||||
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
|
||||
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
|
||||
if (u8len <= 0) return "";
|
||||
std::string out(u8len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &out[0], u8len, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 从 UTF-8 转为目标编码 cp(文件链路的目录路径在 Windows 走 ANSI,与 FileManagerDlg 的
|
||||
// CString 一致;cp 按 clientType 判定:Windows=936、LNX/MAC=CP_UTF8,见 BuildListFiles)。
|
||||
// 非 UTF-8(如纯 ASCII)失败时原样返回。
|
||||
std::string ToAnsi(const std::string& utf8, UINT cp) {
|
||||
if (utf8.empty()) return "";
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), NULL, 0);
|
||||
if (wlen <= 0) return utf8;
|
||||
std::wstring w(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), (int)utf8.size(), &w[0], wlen);
|
||||
int alen = WideCharToMultiByte(cp, 0, w.c_str(), wlen, NULL, 0, NULL, NULL);
|
||||
if (alen <= 0) return utf8;
|
||||
std::string out(alen, '\0');
|
||||
WideCharToMultiByte(cp, 0, w.c_str(), wlen, &out[0], alen, NULL, NULL);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 从 "title|status|pid|r1|r2" 解析(自末尾 4 个 '|' 反推),兼容只有标题的老客户端。
|
||||
// 逻辑复刻 SystemDlg.cpp::ParseWindowAttrs。
|
||||
void ParseWindowAttrsStr(const char* s, std::string& title, std::string& status, uint64_t& pid) {
|
||||
@@ -647,6 +521,10 @@ Json::Value BuildHostItemSchema() {
|
||||
Json::Value onlineProp(Json::objectValue);
|
||||
onlineProp["type"] = "boolean";
|
||||
itemProps["online"] = onlineProp;
|
||||
// 客户端登录时上报的 TLS 传输标志(老客户端恒为 false)
|
||||
Json::Value tlsProp(Json::objectValue);
|
||||
tlsProp["type"] = "boolean";
|
||||
itemProps["tls"] = tlsProp;
|
||||
return itemProps;
|
||||
}
|
||||
|
||||
@@ -1311,7 +1189,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
tool["name"] = "list_online_hosts";
|
||||
// 说明文字为 UTF-8:项目 /execution-charset:.936 会把普通窄字面量编译成 GBK,
|
||||
// 故用 u8 前缀确保输出到 JSON 的字节是 UTF-8。
|
||||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。";
|
||||
tool["description"] = u8"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,非实时探测;老客户端恒为 false)。";
|
||||
|
||||
Json::Value inputSchema(Json::objectValue);
|
||||
inputSchema["type"] = "object";
|
||||
@@ -1328,7 +1206,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "search_hosts";
|
||||
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。";
|
||||
tool["description"] = u8"按计算机名/备注、IP、分组、操作系统过滤在线主机。所有条件均可选、按 AND 组合;子串匹配(ASCII 不区分大小写)。只返回在线主机。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
|
||||
|
||||
tool["inputSchema"] = BuildSearchHostsInputSchema();
|
||||
tool["outputSchema"] = BuildHostOutputSchema();
|
||||
@@ -1340,7 +1218,7 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
{
|
||||
Json::Value tool(Json::objectValue);
|
||||
tool["name"] = "get_host_detail";
|
||||
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。";
|
||||
tool["description"] = u8"获取单台在线主机的详细信息(id、计算机名、IP、操作系统、备注、分组、活动窗口、屏幕分辨率、客户端类型等)。tls=true 表示该客户端登录时使用了 TLS 加密传输(登录时上报,老客户端恒为 false)。";
|
||||
|
||||
tool["inputSchema"] = BuildGetHostDetailInputSchema();
|
||||
tool["outputSchema"] = BuildHostDetailOutputSchema();
|
||||
@@ -1607,6 +1485,11 @@ std::string BuildToolsListResult(const Json::Value& id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 19) 连接管理 / 主机管理(P8:list_network_connections / kill_network_connection /
|
||||
// host_* 共 16 个工具)。开关在 McpMachine.cpp 内部按 McpMachine /
|
||||
// McpMachineDangerous / McpReadonly 逐项判断,关闭时不追加任何工具。
|
||||
McpMachine::AppendTools(tools);
|
||||
|
||||
result["tools"] = tools;
|
||||
return BuildResult(id, result);
|
||||
}
|
||||
@@ -3366,21 +3249,6 @@ static bool GetNormCoord(const Json::Value& args, const char* key, double& out,
|
||||
return true;
|
||||
}
|
||||
|
||||
// 读取可选整数参数(JSON number 或数字字符串);缺失/非法返回 false(不写 out)。
|
||||
static bool GetIntArg(const Json::Value& args, const char* key, int& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
const Json::Value& v = args[key];
|
||||
if (v.isNumeric()) { out = v.asInt(); return true; }
|
||||
if (v.isString()) {
|
||||
const std::string s = v.asString();
|
||||
if (IsDigits(s) || (!s.empty() && s[0] == '-' && IsDigits(s.substr(1)))) {
|
||||
out = atoi(s.c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// button 字符串 → 0=左/1=中/2=右;空或 left 默认左。
|
||||
static bool ParseButton(const std::string& s, int& out) {
|
||||
if (s.empty() || s == "left") { out = 0; return true; }
|
||||
@@ -4351,31 +4219,6 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
|
||||
|
||||
// ========== P7:设备管理控制 ==========
|
||||
|
||||
// 读取 JSON number 或纯数字字符串为 uint64。
|
||||
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
|
||||
if (v.isNumeric()) { out = v.asUInt64(); return true; }
|
||||
if (v.isString()) {
|
||||
std::string s = v.asString();
|
||||
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
|
||||
if (!args.isMember(key)) return false;
|
||||
return ReadUInt64(args[key], out);
|
||||
}
|
||||
|
||||
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
|
||||
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
|
||||
const std::string& tool, const std::string& detail) {
|
||||
if (!parent) return;
|
||||
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
|
||||
parent->PostMessageA(WM_SHOWERRORMSG,
|
||||
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
|
||||
(LPARAM)new CString(_TR("MCP管理操作")));
|
||||
}
|
||||
|
||||
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
|
||||
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
|
||||
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
|
||||
@@ -4687,6 +4530,9 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
|
||||
|
||||
const Json::Value args = GetCallArguments(params);
|
||||
|
||||
// P8:连接管理 / 主机管理工具族(独立分发;门控在 McpMachine::Call 内复核)。
|
||||
if (McpMachine::Handles(toolName)) return McpMachine::Call(id, toolName, args, parent);
|
||||
|
||||
if (toolName == "list_online_hosts") return BuildListOnlineHosts(id, parent);
|
||||
if (toolName == "search_hosts") return BuildSearchHosts(id, args, parent);
|
||||
if (toolName == "get_host_detail") return BuildGetHostDetail(id, args, parent);
|
||||
@@ -5225,13 +5071,18 @@ void CMcpServer::HandleMcp(const httplib::Request& req, httplib::Response& res)
|
||||
|
||||
bool CMcpServer::IsPending(uint64_t device_id) {
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
return m_Pending.find(device_id) != m_Pending.end();
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) return false;
|
||||
// P8:主机管理/连接管理会话的响应包走 OnManagedSubPacket,不归 MFC 的
|
||||
// TOKEN_PSLIST/TOKEN_WSLIST/SERVERLIST/DRIVE_LIST 分支消费(否则会抢走 UI 包)。
|
||||
return it->second.kind == PendingKind::Generic || it->second.kind == PendingKind::FollowUp;
|
||||
}
|
||||
|
||||
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
|
||||
std::lock_guard<std::mutex> lk(m_PendingMutex);
|
||||
auto it = m_Pending.find(device_id);
|
||||
if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
|
||||
if (it->second.kind != PendingKind::Generic && it->second.kind != PendingKind::FollowUp) return; // P8 会话:不接管
|
||||
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
|
||||
it->second.data.assign(data, data + len);
|
||||
it->second.done = true;
|
||||
@@ -5416,6 +5267,7 @@ bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& too
|
||||
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
|
||||
PendingRequest r;
|
||||
r.tool = tool;
|
||||
r.kind = PendingKind::FollowUp;
|
||||
r.followUp = followUp;
|
||||
m_Pending[device_id] = std::move(r);
|
||||
return true;
|
||||
@@ -5501,6 +5353,8 @@ void CMcpServer::OnManageSubClosed(context* subCtx) {
|
||||
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
|
||||
for (auto& kv : m_Pending) {
|
||||
if (kv.second.subCtx == subCtx) {
|
||||
// P8 会话由 OnManagedContextClosed 处理(这里若清 subCtx 会让其无法按指针反查设备)。
|
||||
if (kv.second.kind == PendingKind::Machine || kv.second.kind == PendingKind::ConnMgr) continue;
|
||||
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
|
||||
(void*)subCtx, kv.second.tool.c_str());
|
||||
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
|
||||
|
||||
@@ -270,6 +270,41 @@ public:
|
||||
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
|
||||
void OnManageSubClosed(context* subCtx);
|
||||
|
||||
// ===== P8:主机管理 / 连接管理(子连接会话)=====
|
||||
// 协议:主连接下发 COMMAND_MACHINE_MANAGE(162) / COMMAND_CONNECTION(253) → 客户端新建
|
||||
// 子连接 → 子连接回就绪包(TOKEN_SYSINFOLIST=61 / TOKEN_CONNECTION_LIST=255)→ 在同一
|
||||
// 子连接上多步收发。会话复用 m_Pending 表(统一「单设备单飞行」),kind 区分工具族。
|
||||
// 见 docs/Mcp_Machine_Design.md。
|
||||
void SetMachineEnabled(bool enabled) { m_machineEnabled = enabled; }
|
||||
bool IsMachineEnabled() const { return m_machineEnabled; }
|
||||
void SetMachineDangerousEnabled(bool enabled) { m_machineDangerousEnabled = enabled; }
|
||||
bool IsMachineDangerousEnabled() const { return m_machineDangerousEnabled; }
|
||||
|
||||
// MessageHandle 顶部钩子(终端拦截之后、switch 之前):true = 本包已由 MCP 消费。
|
||||
// 已绑定的子连接:只保存期望 token 的包与 TOKEN_MACHINE_MSG(42),其余丢弃。
|
||||
// 未绑定的子连接:仅 61/255 可作为就绪包尝试绑定;否则返回 false(MFC 行为不变)。
|
||||
bool OnManagedSubPacket(context* subCtx, const BYTE* data, ULONG len);
|
||||
|
||||
// OfflineProc 钩子:接管的子连接断开 → 置 closed 唤醒等待者(幂等)。
|
||||
void OnManagedContextClosed(context* subCtx);
|
||||
|
||||
// 工具线程:登记会话(false = 该设备已有挂起请求在飞 → 设备忙)。
|
||||
bool BeginMachineSession(uint64_t device_id, const std::string& tool, context* mainCtx);
|
||||
bool BeginConnMgrSession(uint64_t device_id, const std::string& tool, context* mainCtx);
|
||||
|
||||
// 工具线程:发命令前清空上一次响应并登记期望 token。
|
||||
void ArmManagedResponse(uint64_t device_id, BYTE expectedToken);
|
||||
|
||||
// 工具线程:等待子连接就绪包 / 指定 token 的响应(超时由调用方按步给)。
|
||||
enum class ManagedWaitResult { Ok, Timeout, Closed, ClientError };
|
||||
ManagedWaitResult WaitManagedPacket(uint64_t device_id, BYTE token,
|
||||
std::vector<BYTE>& out, context*& subCtxOut,
|
||||
int timeoutMs, std::string& clientError);
|
||||
|
||||
// 工具线程:结束会话;cancelIo = 关闭子连接;markOrphan = 打 5s 墓碑
|
||||
// (吸收迟到的 61/255,避免超时后弹出 MFC 对话框)。
|
||||
void EndManagedSession(uint64_t device_id, bool cancelIo, bool markOrphan);
|
||||
|
||||
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
|
||||
void SetReadonly(bool readonly) { m_readonly = readonly; }
|
||||
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
|
||||
@@ -285,6 +320,15 @@ private:
|
||||
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
|
||||
void HandleMcp(const httplib::Request& req, httplib::Response& res);
|
||||
|
||||
// 挂起请求类型:主机管理/连接管理会话与既有一次性请求共用 m_Pending 保证单设备单飞行,
|
||||
// 但 MessageHandle 的 P2b/P3/P7 拦截分支(IsPending/TakeMainResponse)必须把这两类排除,
|
||||
// 否则用户手动打开对话框时 UI 包会被 MCP 抢走。
|
||||
enum class PendingKind { Generic, FollowUp, Machine, ConnMgr };
|
||||
static bool KindAcceptsToken(PendingKind kind, BYTE token);
|
||||
|
||||
bool BeginManagedSession(uint64_t device_id, const std::string& tool,
|
||||
PendingKind kind, context* mainCtx);
|
||||
|
||||
httplib::Server m_server;
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_running{false};
|
||||
@@ -306,11 +350,43 @@ private:
|
||||
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
|
||||
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
|
||||
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
|
||||
// ===== P8:主机管理 / 连接管理会话(kind = Machine / ConnMgr 时有效)=====
|
||||
PendingKind kind = PendingKind::Generic;
|
||||
context* mainCtx = nullptr; // Begin 时抓取的主连接(只作身份校验,不从其收发)
|
||||
std::string peer; // mainCtx->GetPeerName()(老客户端子连接 ID=0 时兜底)
|
||||
BYTE expectedToken = 0; // 期望的响应 token(0 = 未设置)
|
||||
bool hasResponse = false;
|
||||
std::vector<BYTE> response; // 含首字节 token
|
||||
std::string clientError; // TOKEN_MACHINE_MSG(42) 文本(已转 UTF-8)
|
||||
bool closed = false; // 子连接在等待期间断开
|
||||
std::chrono::steady_clock::time_point deadline; // 整个会话的截止时间
|
||||
};
|
||||
std::mutex m_PendingMutex;
|
||||
std::condition_variable m_PendingCv;
|
||||
std::map<uint64_t, PendingRequest> m_Pending;
|
||||
|
||||
// ===== P8:子连接路由(受 m_PendingMutex 保护)=====
|
||||
// 已绑定子连接 → device(该子连接的一切包都由 MCP 消费)。
|
||||
std::map<context*, uint64_t> m_SubCtxToDevice;
|
||||
// CancelIO 之后仍可能到达的包:静默丢弃(避免子连接回收前的尾巴包弹对话框)。
|
||||
std::set<context*> m_AbsorbedSubCtx;
|
||||
// 会话异常结束后 5s 内的迟到就绪包墓碑(device / peer 两个维度)。
|
||||
struct OrphanMark {
|
||||
std::chrono::steady_clock::time_point expiry;
|
||||
PendingKind kind = PendingKind::Generic;
|
||||
};
|
||||
std::map<uint64_t, OrphanMark> m_OrphanByDevice;
|
||||
std::map<std::string, OrphanMark> m_OrphanByPeer;
|
||||
|
||||
// 清理过期墓碑(持 m_PendingMutex 调用)。
|
||||
void PruneOrphanMarksLocked(std::chrono::steady_clock::time_point now);
|
||||
|
||||
// 路由表大小(已绑定 + 已吸收的子连接数)。MessageHandle 每个包都会调用
|
||||
// OnManagedSubPacket,若无会话在飞则用它无锁快速返回,避免音频/屏幕等高频
|
||||
// 子连接逐包争用 m_PendingMutex。只在持锁时更新(store)。
|
||||
std::atomic<int> m_ManagedRoutes{0};
|
||||
void RefreshManagedRoutesLocked();
|
||||
|
||||
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
|
||||
std::atomic<uint16_t> m_PreviewReqId{1};
|
||||
|
||||
@@ -381,6 +457,8 @@ private:
|
||||
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
|
||||
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
|
||||
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
|
||||
bool m_machineEnabled = false; // P8 主机管理族总开关(默认关;16 个 host_* 工具)
|
||||
bool m_machineDangerousEnabled = false; // P8 高风险子集(冻结/解冻进程、停止/重启服务)
|
||||
};
|
||||
|
||||
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
|
||||
|
||||
@@ -106,7 +106,7 @@ INT_PTR CMcpSettingsDlg::DoModal()
|
||||
{
|
||||
USES_CONVERSION;
|
||||
CString title = _TR("MCP设置");
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 420);
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 320, 480);
|
||||
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||
return CDialog::DoModal();
|
||||
}
|
||||
@@ -148,6 +148,12 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MANAGE);
|
||||
m_btnMachine.Create(_TR("启用主机管理(进程/窗口/服务/任务/网络状态)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MACHINE);
|
||||
m_btnMachineDangerous.Create(_TR("启用危险主机操作(冻结/解冻进程、停止/重启服务)"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_MCP_MACHINE_DANGEROUS);
|
||||
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
|
||||
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
|
||||
@@ -172,6 +178,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnMachine.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnMachineDangerous.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
@@ -189,6 +197,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
|
||||
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
|
||||
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
|
||||
int machine = THIS_CFG.GetInt("settings", "McpMachine", 0);
|
||||
int machineDangerous = THIS_CFG.GetInt("settings", "McpMachineDangerous", 0);
|
||||
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
|
||||
|
||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||
@@ -200,6 +210,8 @@ BOOL CMcpSettingsDlg::OnInitDialog()
|
||||
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnMachine.SetCheck(machine ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_btnMachineDangerous.SetCheck(machineDangerous ? BST_CHECKED : BST_UNCHECKED);
|
||||
// 白名单存储为逗号分隔,展示为每行一条。
|
||||
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
|
||||
|
||||
@@ -220,6 +232,16 @@ void CMcpSettingsDlg::OnOK()
|
||||
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
|
||||
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
|
||||
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
|
||||
bool machine = (m_btnMachine.GetCheck() == BST_CHECKED);
|
||||
bool machineDangerous = (m_btnMachineDangerous.GetCheck() == BST_CHECKED);
|
||||
|
||||
// 危险子集依附于主机管理总开关:没开主机管理就没有工具可放开,直接清除并提示。
|
||||
if (machineDangerous && !machine) {
|
||||
machineDangerous = false;
|
||||
m_btnMachineDangerous.SetCheck(BST_UNCHECKED);
|
||||
MessageBoxL(_TR("危险主机操作依附于主机管理:请先启用主机管理。"), _TR("提示"),
|
||||
MB_ICONWARNING);
|
||||
}
|
||||
|
||||
// 端口校验:1-65535
|
||||
int port = atoi(CT2A(sPort));
|
||||
@@ -247,14 +269,17 @@ void CMcpSettingsDlg::OnOK()
|
||||
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpMachine", machine ? 1 : 0);
|
||||
THIS_CFG.SetInt("settings", "McpMachineDangerous", machineDangerous ? 1 : 0);
|
||||
std::string whitelist = CT2A(sWhitelist);
|
||||
whitelist = NormalizeWhitelist(whitelist);
|
||||
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
|
||||
|
||||
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
// 拆成多段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
|
||||
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
|
||||
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。") + _T("\r\n") +
|
||||
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。") + _T("\r\n") +
|
||||
_TR("主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
|
||||
CDialog::OnOK();
|
||||
@@ -300,6 +325,12 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
|
||||
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_btnMachine.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
m_btnMachineDangerous.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
const int whitelistH = 90;
|
||||
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
|
||||
|
||||
@@ -32,6 +32,8 @@ private:
|
||||
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
|
||||
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
|
||||
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
|
||||
IDC_MCP_MACHINE = 1011, // 「启用主机管理」复选框(host_* 工具族)
|
||||
IDC_MCP_MACHINE_DANGEROUS = 1012, // 「启用危险主机操作」复选框(冻结/停止/重启)
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
@@ -42,6 +44,8 @@ private:
|
||||
CButton m_btnRemoteControl;
|
||||
CButton m_btnFileTransfer;
|
||||
CButton m_btnManage;
|
||||
CButton m_btnMachine;
|
||||
CButton m_btnMachineDangerous;
|
||||
CStatic m_lblWhitelist;
|
||||
CEdit m_editWhitelist;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
@@ -205,7 +205,8 @@ void NotifyManager::BuildHostOnlineEmail(context* ctx, const std::string& matche
|
||||
// Get host info
|
||||
std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8);
|
||||
std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8);
|
||||
std::string location = CT2A(ctx->GetClientData(ONLINELIST_LOCATION), CP_UTF8);
|
||||
// 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
|
||||
std::string location = ctx->GetClientData(ONLINELIST_LOCATION).GetString();
|
||||
std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8);
|
||||
std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8);
|
||||
|
||||
|
||||
@@ -102,6 +102,10 @@ void CPluginSettingsDlg::InitListCtrl()
|
||||
GetDlgItem(IDC_STATIC_PLUGIN_INTERVAL)->SetWindowText(_TR("间隔(秒):"));
|
||||
GetDlgItem(IDC_STATIC_PLUGIN_COUNTER)->SetWindowText(_TR("最大次数:"));
|
||||
SetWindowText(_TR("插件设置"));
|
||||
|
||||
// 按钮文字来自对话框模板,非中文系统上会被系统 ACP 转成 "??",须显式设置(参照 NotifySettingsDlg)
|
||||
SetDlgItemText(IDC_BTN_SAVE, _TR("保存"));
|
||||
SetDlgItemText(IDOK, _TR("关闭"));
|
||||
}
|
||||
|
||||
void CPluginSettingsDlg::LoadPluginsToList()
|
||||
|
||||
@@ -124,11 +124,31 @@ protected:
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
|
||||
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
|
||||
// 均经 _TR/_L 支持多语言翻译。
|
||||
SetWindowText(_TR("远程桌面质量等级"));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_LEVEL, _TR("等级"));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_FPS, _TR("帧率(FPS)"));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_WIDTH, _TR("最大宽度"));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_ALGO, _TR("编码算法"));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_BITRATE, _TR("码率(kbps)"));
|
||||
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_QLEVEL_LABEL_LEVEL0 + i, _L(kLevelNames[i]));
|
||||
SetDlgItemText(IDC_QLEVEL_LABEL_HINT,
|
||||
_TR("最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网"));
|
||||
SetDlgItemText(IDC_QLEVEL_RESET, _TR("恢复默认"));
|
||||
SetDlgItemText(IDOK, _TR("确定"));
|
||||
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = m_profiles[i];
|
||||
}
|
||||
LoadToControls();
|
||||
TranslateDialog(this);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1126,6 +1126,10 @@ VOID CScreenSpyDlg::OnReceiveComplete()
|
||||
case TOKEN_CLIPBOARD_TEXT: {
|
||||
Buffer str = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(1);
|
||||
UpdateServerClipboard(str.c_str(), str.length());
|
||||
// 反向回传:远程剪贴板文本(UTF-8)→ Web 客户端
|
||||
if (m_bIsWebSession && WebService().IsRunning()) {
|
||||
WebService().NotifyClipboardText(m_ClientID, std::string(str.c_str(), str.length()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOKEN_BITMAPINFO: {
|
||||
@@ -3643,7 +3647,7 @@ void CScreenSpyDlg::OnDropFiles(HDROP hDropInfo)
|
||||
memcpy((char*)szBuffer + 1 + masterId.length(), hmac.c_str(), hmac.length());
|
||||
memcpy(szBuffer + 1 + 80, str.data(), str.size());
|
||||
auto md5 = CalcMD5FromBytes((BYTE*)str.data(), str.size());
|
||||
m_pParent->m_CmdList.PutCmd(md5);
|
||||
m_pParent->m_CmdList.PutCmd(md5, m_ContextObject->GetPeerName());
|
||||
m_ContextObject->Send2Client(szBuffer, 81 + str.size());
|
||||
Mprintf("【拖拽】 [本地 -> 远程] V1 传输: %s\n", md5.c_str());
|
||||
SAFE_DELETE_ARRAY(szBuffer);
|
||||
|
||||
@@ -277,6 +277,12 @@ void CSearchBarDlg::DoSearch()
|
||||
colText = ctx->GetClientData(col);
|
||||
}
|
||||
|
||||
// 地理位置列已统一按 UTF-8 存储(见 AddList),而搜索输入是 CP_ACP;
|
||||
// 转回 CP_ACP 再比对,否则按中文位置搜索会因编码不一致而失配。
|
||||
if (col == ONLINELIST_LOCATION) {
|
||||
colText = CString(Utf8ToAnsi(std::string(colText.GetString())).c_str());
|
||||
}
|
||||
|
||||
colText.MakeLower();
|
||||
if (colText.Find(searchText) >= 0) {
|
||||
matched = true;
|
||||
|
||||
@@ -329,6 +329,17 @@ public:
|
||||
#define PREALLOC_DECOMPRESS_SIZE (4 * 1024)
|
||||
|
||||
#include "context.h"
|
||||
|
||||
// 每连接传输层状态(单向 TLS,见 docs/TransportTLS_MFCServer_Design.md §5.2)。
|
||||
// 明文与 TLS 客户端在同一端口共存:首包按 ClientHello 头嗅探分派。
|
||||
enum ServerTlsState {
|
||||
TLS_Unclassified = 0, // 尚未收到足够字节判定(缓冲 tlsPreClassify)
|
||||
TLS_Plain = 1, // 明文客户端,直通现有解析路径
|
||||
TLS_Handshaking = 2, // TLS 客户端,握手线程阻塞 AcceptSecurityContext 中
|
||||
TLS_Ready = 3, // TLS 握手完成,收包解密 / 发包加密
|
||||
};
|
||||
class ServerTlsSession; // 定义见 ServerTlsSession.h(避免把 Schannel 头拖进本文件)
|
||||
|
||||
typedef class CONTEXT_OBJECT : public context
|
||||
{
|
||||
public:
|
||||
@@ -341,6 +352,7 @@ public:
|
||||
FreeDecompressBuffer();
|
||||
FreeCompressBuffer();
|
||||
FreeSendCompressBuffer();
|
||||
FreeTls();
|
||||
if (Zcctx) {
|
||||
ZSTD_freeCCtx(Zcctx);
|
||||
Zcctx = nullptr;
|
||||
@@ -377,6 +389,11 @@ public:
|
||||
ikcpcb* kcp = nullptr; // 新增,指向KCP会话
|
||||
std::string GroupName; // 分组名称
|
||||
CLock SendLock; // fix #214
|
||||
// 发送是否处于「进行中」状态。OnClientPreSending 每次 Send2Client 都会 Post 一个
|
||||
// IOWrite,若不加节制,OnClientPostSending(0) 会对同一 OutCompressedBuffer 并发发起
|
||||
// 多个 WSASend,读到同一段可变内存(背靠背小包最易触发:resize→COMMAND_NEXT→resize)。
|
||||
// 置位后抑制多余的 IOWrite,待缓冲排空(最后一个 WSASend 完成)再清零。仅受 SendLock 保护。
|
||||
bool bSendPending = false;
|
||||
time_t OnlineTime = 0; // 上线时间
|
||||
time_t LastHeartbeatTime = 0; // 最后心跳时间
|
||||
|
||||
@@ -392,6 +409,11 @@ public:
|
||||
// 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。
|
||||
TcpRttBreachDetector m_RttDetector;
|
||||
|
||||
// ---- 单向 TLS 状态(跨线程:握手线程写入 TLS_Ready,工作线程读取;用 atomic 保序)----
|
||||
std::atomic<int> tlsState{ TLS_Unclassified }; // ServerTlsState
|
||||
ServerTlsSession* tls = nullptr; // 每连接 TLS 会话(握手线程独占→就绪后共用)
|
||||
std::vector<BYTE> tlsPreClassify; // 分类前缓冲的首批字节(嗅探 ClientHello 头)
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
@@ -495,6 +517,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// 释放每连接 TLS 会话。定义在 ServerTlsSession.cpp(需 ServerTlsSession 完整类型才能 delete)。
|
||||
void FreeTls();
|
||||
|
||||
std::string GetProtocol() const override
|
||||
{
|
||||
return Parser.m_Masker && Parser.m_Masker->GetMaskType() == MaskTypeNone ? "TCP" : "HTTP";
|
||||
@@ -539,6 +564,12 @@ public:
|
||||
m_RttUs.store(0, std::memory_order_release);
|
||||
m_LastRttSampleMs.store(0, std::memory_order_release);
|
||||
m_RttDetector.Reset();
|
||||
// 复用对象池时重置 TLS 状态:释放旧会话、回到未分类、清空嗅探缓冲。
|
||||
FreeTls();
|
||||
tlsState.store(TLS_Unclassified, std::memory_order_release);
|
||||
tlsPreClassify.clear();
|
||||
// 复用对象池时清空发送进行中标志,避免上次连接残留的 bSendPending 抑制新连接的发送。
|
||||
bSendPending = false;
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
// ServerTlsSession.cpp — 服务端单向 TLS(Schannel 入站)实现
|
||||
//
|
||||
// 与客户端 TlsChannel.cpp 对称,但走 AcceptSecurityContext 入站握手、DecryptMessage/
|
||||
// EncryptMessage 流式加解密。证书/私钥加载为 PEM + CNG(NCryptImportKey PKCS#8),
|
||||
// 以便与 Go 服务端(server/go 的 crypto/tls,YAMA_TLS_CERT/KEY)共用同一对密钥。
|
||||
#include "StdAfx.h"
|
||||
#include "ServerTlsSession.h"
|
||||
#include "Server.h" // CONTEXT_OBJECT::FreeTls()
|
||||
#include "common/iniFile.h" // config(GetInt/GetStr)
|
||||
|
||||
#include <ncrypt.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
#pragma comment(lib, "secur32.lib")
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
#pragma comment(lib, "ncrypt.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
// 进程级身份(进程启动加载一次;未来 UI/指令热切换时改为引用计数 + 惰性释放)。
|
||||
std::mutex g_identityMtx;
|
||||
bool g_enabled = false;
|
||||
bool g_ready = false;
|
||||
bool g_loadError = false;
|
||||
CredHandle g_cred = {};
|
||||
bool g_credValid = false;
|
||||
PCCERT_CONTEXT g_cert = NULL; // 与入站凭据同生命周期,FreeIdentity 时释放
|
||||
NCRYPT_KEY_HANDLE g_persistedKey = 0; // 已持久化到 CNG KSP 的私钥句柄,FreeIdentity 时删除
|
||||
|
||||
// 持久化私钥在 CNG KSP 中的名字。用固定名 + NCRYPT_OVERWRITE_KEY_FLAG,重启时覆盖旧键,
|
||||
// 避免残留;同名键的密钥材料与 server.key 相同,覆盖无副作用。
|
||||
const wchar_t* g_keyName = L"YamaServerTlsKey";
|
||||
|
||||
// 一次性读文件为 std::string(二进制安全)。
|
||||
bool ReadFileText(const std::string& path, std::string& out)
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
if (f.bad())
|
||||
return false;
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
// PEM(-----BEGIN ...----- 包裹的 base64)解码为 DER。失败返回 false。
|
||||
// CRYPT_STRING_ANY 会自动识别带头的 base64(PEM),对证书与 PKCS#8 私钥均适用。
|
||||
bool PemToDer(const std::string& pem, std::vector<BYTE>& der)
|
||||
{
|
||||
if (pem.empty())
|
||||
return false;
|
||||
DWORD need = 0;
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
NULL, &need, NULL, NULL))
|
||||
return false;
|
||||
der.resize(need);
|
||||
return CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
der.data(), &need, NULL, NULL) == TRUE;
|
||||
}
|
||||
|
||||
// 从 PEM 文本里定位私钥块的类型,用于给出可操作的错误提示。
|
||||
enum KeyFmt { KF_PKCS8, KF_PKCS1, KF_UNKNOWN };
|
||||
KeyFmt DetectKeyFormat(const std::string& keyPem)
|
||||
{
|
||||
if (keyPem.find("-----BEGIN PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS8;
|
||||
if (keyPem.find("-----BEGIN RSA PRIVATE KEY-----") != std::string::npos ||
|
||||
keyPem.find("-----BEGIN EC PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS1;
|
||||
return KF_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsIdentity —— 进程级服务端身份(可重入加载,禁止 call_once,供未来热切换)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsIdentity::FreeIdentity()
|
||||
{
|
||||
// 先释放凭据,再释放证书上下文,最后删除持久化私钥(凭据/证书可能仍按名引用该键)。
|
||||
if (g_credValid)
|
||||
{
|
||||
FreeCredentialsHandle(&g_cred);
|
||||
g_credValid = false;
|
||||
}
|
||||
g_cred = {};
|
||||
if (g_cert)
|
||||
{
|
||||
CertFreeCertificateContext(g_cert);
|
||||
g_cert = NULL;
|
||||
}
|
||||
if (g_persistedKey)
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::Load(config& cfg)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
|
||||
bool enabled = cfg.GetInt("tls", "Enabled", 0) != 0;
|
||||
std::string certPath = cfg.GetStr("tls", "CertPath", "");
|
||||
std::string keyPath = cfg.GetStr("tls", "KeyPath", "");
|
||||
|
||||
g_enabled = enabled;
|
||||
|
||||
// 先释放旧身份(本轮只在启动时调用一次,不存在「旧身份仍被并发握手使用」的问题;
|
||||
// 未来热切换需先引用计数、再惰性释放旧凭据)。
|
||||
FreeIdentity();
|
||||
g_ready = false;
|
||||
g_loadError = false;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
Mprintf("[TLS] server TLS disabled ([tls] Enabled=0) — plaintext only\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (certPath.empty() || keyPath.empty())
|
||||
{
|
||||
g_loadError = true;
|
||||
Mprintf("[TLS] FATAL: [tls] Enabled=1 but CertPath/KeyPath empty; "
|
||||
"refusing TLS ClientHello (fail-closed)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadIdentity(certPath, keyPath))
|
||||
{
|
||||
g_loadError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ready = true;
|
||||
Mprintf("[TLS] server TLS enabled: cert=%s key=%s\n", certPath.c_str(), keyPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::LoadIdentity(const std::string& certPath, const std::string& keyPath)
|
||||
{
|
||||
std::string certPem, keyPem;
|
||||
if (!ReadFileText(certPath, certPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read cert file %s\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!ReadFileText(keyPath, keyPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read key file %s\n", keyPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1) 证书:PEM → DER → PCCERT_CONTEXT。
|
||||
std::vector<BYTE> certDer;
|
||||
if (!PemToDer(certPem, certDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cert file %s is not a valid PEM\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING,
|
||||
certDer.data(), (DWORD)certDer.size());
|
||||
if (cert == NULL)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertCreateCertificateContext failed (%u)\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) 私钥:PEM → DER → 持久化到 CNG KSP(仅支持 PKCS#8;PKCS#1 给出可操作的转换提示)。
|
||||
//
|
||||
// 关键:Schannel 入站凭据(AcquireCredentialsHandle)需要「能按名重开」的持久化私钥。
|
||||
// NCryptImportKey 产出的是匿名临时键,绑定 CERT_NCRYPT_KEY_HANDLE_PROP_ID 后 Schannel 仍
|
||||
// 拿不到(AcquireCredentialsHandle 返回 SEC_E_NO_CREDENTIALS 0x8009030e)。因此先把
|
||||
// PKCS#8 导入为临时键、导出为裸 RSA 私钥 BLOB(NCryptSetProperty 只认裸 BLOB,不认
|
||||
// PKCS#8),再写入持久化键并 finalize,最后用 CERT_KEY_PROV_INFO_PROP_ID 按名引用。
|
||||
std::vector<BYTE> keyDer;
|
||||
NCRYPT_KEY_HANDLE persistedKey = 0;
|
||||
if (DetectKeyFormat(keyPem) == KF_PKCS8)
|
||||
{
|
||||
if (!PemToDer(keyPem, keyDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: key file %s is not a valid PEM\n", keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 显式打开 Microsoft Software Key Storage Provider。hProvider 传 NULL 会让
|
||||
// NCryptImportKey 走「默认提供者」解析,实测返回 NTE_INVALID_HANDLE(0x80090026),
|
||||
// 故必须显式取得 KSP 句柄(临时键独立于 hProv,导入后即可释放)。
|
||||
NCRYPT_PROV_HANDLE hProv = 0;
|
||||
SECURITY_STATUS st = NCryptOpenStorageProvider(&hProv, MS_KEY_STORAGE_PROVIDER, 0);
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: NCryptOpenStorageProvider failed 0x%08x\n", (unsigned)st);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2a) PKCS#8 → 临时键。
|
||||
NCRYPT_KEY_HANDLE tmp = 0;
|
||||
st = NCryptImportKey(hProv, NULL, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL,
|
||||
&tmp, keyDer.data(), (DWORD)keyDer.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
// 2b) 允许明文导出,导出为裸 RSA 私钥 BLOB。
|
||||
DWORD policy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG | NCRYPT_ALLOW_EXPORT_FLAG;
|
||||
NCryptSetProperty(tmp, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&policy, sizeof(policy), 0);
|
||||
|
||||
std::vector<BYTE> raw;
|
||||
DWORD rawLen = 0;
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, NULL, 0, &rawLen, 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
raw.resize(rawLen);
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL,
|
||||
raw.data(), rawLen, &rawLen, 0);
|
||||
}
|
||||
|
||||
// 2c) 写入持久化键并 finalize(落到 KSP,可被 Schannel 按名重开)。
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptCreatePersistedKey(hProv, &persistedKey, BCRYPT_RSA_ALGORITHM,
|
||||
g_keyName, AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptSetProperty(persistedKey, BCRYPT_RSAFULLPRIVATE_BLOB,
|
||||
raw.data(), (DWORD)raw.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptFinalizeKey(persistedKey, 0);
|
||||
|
||||
NCryptFreeObject(tmp);
|
||||
}
|
||||
NCryptFreeObject(hProv);
|
||||
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: failed to persist PKCS#8 key for %s (0x%08x)\n",
|
||||
keyPath.c_str(), (unsigned)st);
|
||||
if (persistedKey)
|
||||
NCryptFreeObject(persistedKey);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// PKCS#1(RSA/EC PRIVATE KEY)或未知格式:本次不支持,给转换命令。
|
||||
Mprintf("[TLS] FATAL: key file %s must be PKCS#8 (-----BEGIN PRIVATE KEY-----). "
|
||||
"Convert with: openssl pkcs8 -topk8 -nocrypt -in %s -out %s\n",
|
||||
keyPath.c_str(), keyPath.c_str(), keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3) 绑定私钥到证书上下文:按名引用持久化键。Schannel 经 CryptAcquireCertificatePrivateKey
|
||||
// 在 MS_KEY_STORAGE_PROVIDER 里用 pwszContainerName 重开该键。
|
||||
CRYPT_KEY_PROV_INFO provInfo = {};
|
||||
provInfo.pwszContainerName = (LPWSTR)g_keyName;
|
||||
provInfo.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
|
||||
provInfo.dwProvType = 0; // 0 = CNG(无 CAPI provider type)
|
||||
provInfo.dwKeySpec = AT_KEYEXCHANGE;
|
||||
if (!CertSetCertificateContextProperty(cert, CERT_KEY_PROV_INFO_PROP_ID, 0, &provInfo))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) "
|
||||
"failed (%u)\n", GetLastError());
|
||||
NCryptDeleteKey(persistedKey, 0); // 删除并释放句柄
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 绑定后持久化键句柄交进程级 g_persistedKey,随 FreeIdentity 删除(覆盖旧键,避免残留)。
|
||||
g_persistedKey = persistedKey;
|
||||
|
||||
// 4) 入站凭据:显式指定服务端证书,不请求客户端证书(单向 TLS)。
|
||||
SCHANNEL_CRED sc = {};
|
||||
sc.dwVersion = SCHANNEL_CRED_VERSION;
|
||||
sc.cCreds = 1;
|
||||
sc.paCred = &cert;
|
||||
sc.grbitEnabledProtocols = SP_PROT_TLS1_2_SERVER;
|
||||
// NO_DEFAULT_CREDS 避免回退到系统默认凭据,确保只用我们提供的证书/私钥。
|
||||
//(单向 TLS 不请求客户端证书,故无需 SCH_CRED_MANUAL_CRED_VALIDATION。)
|
||||
sc.dwFlags = SCH_CRED_NO_DEFAULT_CREDS;
|
||||
|
||||
TimeStamp tsExpiry = {};
|
||||
SECURITY_STATUS st = AcquireCredentialsHandleA(
|
||||
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND,
|
||||
NULL, &sc, NULL, NULL, &g_cred, &tsExpiry);
|
||||
|
||||
if (st != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: AcquireCredentialsHandle(SECPKG_CRED_INBOUND) failed 0x%08x\n",
|
||||
(unsigned)st);
|
||||
CertFreeCertificateContext(cert); // 无凭据引用,安全释放
|
||||
g_cred = {};
|
||||
if (g_persistedKey) // 回滚:删除已持久化的私钥,避免残留
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 成功:把证书上下文生命周期交给进程级 g_cert,随 FreeIdentity 释放,
|
||||
// 保证在 FreeCredentialsHandle 之前私钥一直可用。
|
||||
g_cert = cert;
|
||||
g_credValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsEnabled()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_enabled;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsReady()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_ready && g_credValid;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsLoadError()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_loadError;
|
||||
}
|
||||
|
||||
CredHandle ServerTlsIdentity::Cred()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_cred;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsSession —— 单连接会话(握手 + 加解密)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsSession::CloseHandles()
|
||||
{
|
||||
if (m_ctxValid)
|
||||
{
|
||||
DeleteSecurityContext(&m_ctx);
|
||||
m_ctxValid = false;
|
||||
}
|
||||
m_ctx = {};
|
||||
m_ready = false;
|
||||
m_raw.clear();
|
||||
}
|
||||
|
||||
// 握手线程在 IOCP 的子套接字上做同步 I/O。该套接字可能是非阻塞的(监听套接字经
|
||||
// WSAEventSelect 置为非阻塞后,accept 出的子套接字继承非阻塞态;线程里 FIONBIO=0 也
|
||||
// 不一定即时生效)。故 recv/send 遇 WSAEWOULDBLOCK 时用 select 等可读/可写再重试,
|
||||
// 而非当致命错误,避免「客户端回包还没到就误判握手失败」。
|
||||
static bool WaitSocketReady(SOCKET sock, bool forRead, int timeoutMs)
|
||||
{
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(sock, &fds);
|
||||
timeval tv;
|
||||
tv.tv_sec = timeoutMs / 1000;
|
||||
tv.tv_usec = (timeoutMs % 1000) * 1000;
|
||||
return select(0, forRead ? &fds : NULL, forRead ? NULL : &fds, NULL, &tv) > 0;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::SendAll(SOCKET sock, const void* data, int len)
|
||||
{
|
||||
const BYTE* p = (const BYTE*)data;
|
||||
int sent = 0;
|
||||
while (sent < len)
|
||||
{
|
||||
int n = send(sock, (const char*)p + sent, len - sent, 0);
|
||||
if (n == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, false, 15000))
|
||||
return false;
|
||||
continue; // 非阻塞套接字发送缓冲满:等可写后重试
|
||||
}
|
||||
if (n <= 0)
|
||||
return false;
|
||||
sent += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::Handshake(SOCKET sock, const BYTE* initial, int initialLen)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (m_ctxValid)
|
||||
CloseHandles(); // 防御:会话应只握手一次
|
||||
|
||||
CredHandle cred = ServerTlsIdentity::Cred();
|
||||
|
||||
// 单向 TLS:服务端不请求客户端证书,故无需 ASC_REQ_MANUAL_CRED_VALIDATION
|
||||
//(该标志只在 mTLS 校验客户端证书时有意义;SDK 的 ASC 命名空间里亦无此常量)。
|
||||
DWORD req = ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONFIDENTIALITY |
|
||||
ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_STREAM;
|
||||
|
||||
std::vector<BYTE> hs(initial, initial + initialLen);
|
||||
CtxtHandle ctx = {};
|
||||
bool ctxValid = false;
|
||||
TimeStamp tsExpiry = {};
|
||||
ULONG attrs = 0;
|
||||
SECURITY_STATUS sc = SEC_I_CONTINUE_NEEDED;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer inBufs[2] = {
|
||||
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.empty() ? NULL : hs.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
|
||||
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
|
||||
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
|
||||
|
||||
sc = AcceptSecurityContext(&cred, ctxValid ? &ctx : NULL, &inDesc, req,
|
||||
SECURITY_NATIVE_DREP, ctxValid ? NULL : &ctx,
|
||||
&outDesc, &attrs, &tsExpiry);
|
||||
ctxValid = true;
|
||||
|
||||
// 发出服务端握手记录(ServerHello/Certificate/... 或 Finished)。
|
||||
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer)
|
||||
{
|
||||
bool ok = SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer);
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
if (!ok)
|
||||
{
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算已消费字节:SECBUFFER_EXTRA 里是「未消费的尾部」(下一条握手消息 / 首条应用数据)。
|
||||
// 必须在 SEC_E_OK / 失败判定之前处理:否则握手完成时 hs 里仍混着已消费的握手记录,
|
||||
// 紧跟 Finished 的首条应用数据(客户端登录包)会随 hs 一起被丢弃,导致随机丢包。
|
||||
if (inBufs[1].BufferType == SECBUFFER_EXTRA)
|
||||
{
|
||||
int extra = (int)inBufs[1].cbBuffer;
|
||||
hs.erase(hs.begin(), hs.begin() + (hs.size() - extra));
|
||||
}
|
||||
else if (sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
hs.clear(); // 已全部消费,继续等下一轮握手消息
|
||||
}
|
||||
// 否则:SEC_E_INCOMPLETE_MESSAGE —— 保留 hs(不完整消息),续收补齐。
|
||||
|
||||
if (sc == SEC_E_OK)
|
||||
break;
|
||||
if (FAILED(sc) && sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
Mprintf("[TLS] AcceptSecurityContext failed 0x%08x (in=%u consumed? extra=%u)\n",
|
||||
(unsigned)sc, inBufs[0].cbBuffer, inBufs[1].BufferType == SECBUFFER_EXTRA ? inBufs[1].cbBuffer : 0);
|
||||
break; // 真正的失败
|
||||
}
|
||||
|
||||
char raw[8192];
|
||||
int r;
|
||||
for (;;)
|
||||
{
|
||||
r = recv(sock, raw, sizeof(raw), 0);
|
||||
if (r == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, true, 15000))
|
||||
{
|
||||
r = SOCKET_ERROR;
|
||||
break; // 等可读超时
|
||||
}
|
||||
continue; // 非阻塞套接字暂无数据:等可读后重试
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (r <= 0)
|
||||
{
|
||||
Mprintf("[TLS] handshake recv failed/EOF (r=%d err=%d)\n", r, WSAGetLastError());
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
|
||||
}
|
||||
|
||||
if (sc != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] handshake failed: final status 0x%08x (fail-closed)\n", (unsigned)sc);
|
||||
if (ctxValid)
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 握手成功后 hs 里残留的「未消费尾部」是紧跟 Finished 的首条应用数据(TLS 记录)。
|
||||
// 交给 Decrypt 处理,否则首包会丢(表现为随机登录超时/重连/协议错位)。
|
||||
if (!hs.empty())
|
||||
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
|
||||
|
||||
// 查询流式记录尺寸(加密封装用)。
|
||||
SecPkgContext_StreamSizes sizes = {};
|
||||
if (QueryContextAttributes(&ctx, SECPKG_ATTR_STREAM_SIZES, &sizes) != SEC_E_OK)
|
||||
{
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ctx = ctx;
|
||||
m_ctxValid = true;
|
||||
m_sizes = sizes;
|
||||
m_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
m_raw.insert(m_raw.end(), cipher, cipher + len);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer bufs[4] = {
|
||||
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.empty() ? NULL : 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; // 记录不完整,等下一轮 WSARecv
|
||||
if (sc == SEC_I_CONTEXT_EXPIRED) {
|
||||
Mprintf("[TLS] DecryptMessage: peer close_notify (SEC_I_CONTEXT_EXPIRED)\n");
|
||||
return -1; // 对端 close_notify / 会话结束
|
||||
}
|
||||
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
|
||||
Mprintf("[TLS] DecryptMessage failed 0x%08x (raw=%u)\n", (unsigned)sc, (unsigned)m_raw.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
int decLen = (int)bufs[1].cbBuffer;
|
||||
const BYTE* p = (const BYTE*)bufs[1].pvBuffer;
|
||||
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? (int)bufs[3].cbBuffer : 0;
|
||||
int consumed = (int)m_raw.size() - extra;
|
||||
|
||||
if (decLen > 0 && p)
|
||||
plain.insert(plain.end(), p, p + decLen);
|
||||
|
||||
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
|
||||
if (m_raw.empty())
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
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, plain + 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 -1; // 握手已建立,此处失败视为致命(调用方关闭连接)
|
||||
|
||||
int wireLen = (int)(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer);
|
||||
wire.insert(wire.end(), rec.data(), rec.data() + wireLen);
|
||||
total += chunk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// CONTEXT_OBJECT::FreeTls —— 释放每连接 TLS 会话(定义在此以便 delete 完整类型)
|
||||
// =====================================================================================
|
||||
|
||||
void CONTEXT_OBJECT::FreeTls()
|
||||
{
|
||||
delete tls;
|
||||
tls = NULL;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// ServerTlsSession.h — MFC 服务端单向 TLS(Schannel 入站)会话封装
|
||||
//
|
||||
// 与客户端 TlsChannel 对称:客户端走 InitializeSecurityContext(出站),本类走
|
||||
// AcceptSecurityContext(入站)。单向 TLS:不请求/校验客户端证书,客户端身份仍由
|
||||
// 隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
|
||||
//
|
||||
// 分层:
|
||||
// ServerTlsIdentity —— 进程级「服务端身份」(证书 + 私钥 → 入站凭据)。可重入加载
|
||||
// (Load 禁止用 call_once,供未来 UI/指令热切换),本轮启动时加载一次。
|
||||
// ServerTlsSession —— 每条连接一个,持有握手/加解密所需的 Schannel 上下文与缓冲,
|
||||
// 随 CONTEXT_OBJECT 池化复用而重建。
|
||||
//
|
||||
// 线程模型:Handshake 在专用握手线程上阻塞执行(方案 A);握手完成后,Decrypt 由
|
||||
// IOCP 工作线程调用(每连接同一时刻只有一个未决 WSARecv,天然串行)、Encrypt 在
|
||||
// SendLock 内调用。Schannel 上下文非线程安全,故 Encrypt/Decrypt 内部以 m_mtx 互斥。
|
||||
//
|
||||
// 选型:Schannel(Windows 原生 SSPI),secur32.lib / crypt32.lib / ncrypt.lib 系统库。
|
||||
// 证书/私钥为 PEM(与 Go 服务端 YAMA_TLS_CERT/KEY 同构,可共用同一对密钥);私钥
|
||||
// 须为 PKCS#8(-----BEGIN PRIVATE KEY-----),见 ServerTlsSession.cpp 的加载实现。
|
||||
#pragma once
|
||||
|
||||
#include <WinSock2.h>
|
||||
|
||||
#ifndef SECURITY_WIN32
|
||||
#define SECURITY_WIN32 1
|
||||
#endif
|
||||
#include <security.h>
|
||||
#include <schannel.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class config; // common/iniFile.h(仅需引用,避免把 ini 头拖进 Server.h 的 include 图)
|
||||
|
||||
// ---- 进程级服务端身份(证书 + 私钥),可重入加载(禁止 call_once,供未来热切换)----
|
||||
class ServerTlsIdentity
|
||||
{
|
||||
public:
|
||||
// 从 cfg(THIS_CFG)读 [tls] 节:Enabled / CertPath / KeyPath,并加载身份。
|
||||
// 返回 true 表示「配置已生效」(开关关、或开关开且身份加载成功);开关开但加载
|
||||
// 失败时返回 false 且 IsLoadError()==true,服务端须对 TLS ClientHello fail-closed。
|
||||
static bool Load(config& cfg);
|
||||
|
||||
static bool IsEnabled(); // [tls] Enabled != 0
|
||||
static bool IsReady(); // 开关开 && 身份加载成功(有可用入站凭据)
|
||||
static bool IsLoadError(); // 开关开 && 身份加载失败(fail-closed)
|
||||
|
||||
// 入站凭据(AcquireCredentialsHandle 产出)。返回拷贝;句柄值本身可安全拷贝。
|
||||
static CredHandle Cred();
|
||||
|
||||
private:
|
||||
static void FreeIdentity();
|
||||
static bool LoadIdentity(const std::string& certPath, const std::string& keyPath);
|
||||
};
|
||||
|
||||
// ---- 单连接服务端 TLS 会话(握手 + 加解密)----
|
||||
class ServerTlsSession
|
||||
{
|
||||
public:
|
||||
ServerTlsSession() {}
|
||||
~ServerTlsSession() { CloseHandles(); }
|
||||
|
||||
ServerTlsSession(const ServerTlsSession&) = delete;
|
||||
ServerTlsSession& operator=(const ServerTlsSession&) = delete;
|
||||
|
||||
// 阻塞式服务端握手(方案 A:专用握手线程上调用)。initial/initialLen 是嗅探阶段已
|
||||
// 收到的 ClientHello 字节(可能只是分片;内部按 SEC_E_INCOMPLETE_MESSAGE 续收补齐)。
|
||||
// 成功返回 true,失败 false(调用方关闭连接,fail-closed)。
|
||||
bool Handshake(SOCKET sock, const BYTE* initial, int initialLen);
|
||||
|
||||
// 解密:把本次 WSARecv 收到的密文 cipher[0..len] 喂入,把能解出的明文追加到 plain。
|
||||
// 返回 0 = 成功(可能无明文);-1 = 错误 / 对端 close_notify(调用方关闭连接)。
|
||||
int Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain);
|
||||
|
||||
// 加密:把明文 plain[0..len] 加密成完整 TLS 记录,追加到 wire。
|
||||
// 返回加密的明文字节数(== len);-1 = 错误。
|
||||
int Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire);
|
||||
|
||||
bool Ready() const { return m_ready; }
|
||||
|
||||
private:
|
||||
void CloseHandles();
|
||||
bool SendAll(SOCKET sock, const void* data, int len);
|
||||
|
||||
std::mutex m_mtx; // 序列化对 m_ctx 的 Encrypt/Decrypt(Schannel 非线程安全)
|
||||
CtxtHandle m_ctx = {};
|
||||
bool m_ctxValid = false;
|
||||
bool m_ready = false;
|
||||
SecPkgContext_StreamSizes m_sizes = {};
|
||||
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
|
||||
};
|
||||
@@ -349,6 +349,75 @@ public:
|
||||
void onHttp(HttpHandler handler) { m_httpHandler = handler; }
|
||||
|
||||
private:
|
||||
// ---- Real client IP behind a reverse proxy ----
|
||||
// A same-host nginx/Caddy connects from 127.0.0.1 and reports the original
|
||||
// peer in X-Real-IP / X-Forwarded-For. Only trust those headers when the
|
||||
// TCP peer is loopback: otherwise any internet client could forge its
|
||||
// address in the audit log and rotate it to evade login throttling.
|
||||
static std::string trimHttpValue(const std::string& s) {
|
||||
size_t b = s.find_first_not_of(" \t");
|
||||
if (b == std::string::npos) return "";
|
||||
size_t e = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(b, e - b + 1);
|
||||
}
|
||||
|
||||
static std::string asciiLower(const std::string& s) {
|
||||
std::string out = s;
|
||||
for (char& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Case-insensitive lookup of one header in the raw request. The request
|
||||
// line is first, so matching "\r\n<name>:" cannot hit the request target.
|
||||
static std::string httpHeader(const std::string& request, const char* name) {
|
||||
const std::string needle = "\r\n" + asciiLower(name) + ":";
|
||||
const std::string hay = asciiLower(request);
|
||||
size_t pos = hay.find(needle);
|
||||
if (pos == std::string::npos) return "";
|
||||
size_t vstart = pos + needle.size();
|
||||
size_t vend = hay.find("\r\n", vstart);
|
||||
return trimHttpValue(request.substr(vstart,
|
||||
vend == std::string::npos ? std::string::npos : vend - vstart));
|
||||
}
|
||||
|
||||
// Accept only a bare IPv4/IPv6 literal; forwarded headers are attacker
|
||||
// controlled even when the proxy is trusted, so no hostnames/ports.
|
||||
static std::string normalizeIPLiteral(const std::string& value) {
|
||||
std::string s = trimHttpValue(value);
|
||||
if (s.empty() || s.size() > 63) return "";
|
||||
struct in_addr v4;
|
||||
if (inet_pton(AF_INET, s.c_str(), &v4) == 1) return s;
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET6, s.c_str(), &v6) == 1) return s;
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string forwardedClientIP(const std::string& request) {
|
||||
// X-Forwarded-For is "client, proxy1, ..." with the oldest hop first.
|
||||
// Our own proxy appends the peer it saw, so the last valid entry is
|
||||
// the one it actually observed: a client-supplied list cannot forge
|
||||
// it. Prefer this over X-Real-IP, which is only trustworthy when the
|
||||
// proxy explicitly overwrites it (some configs pass it through).
|
||||
std::string xff = httpHeader(request, "x-forwarded-for");
|
||||
size_t end = xff.size();
|
||||
while (end > 0) {
|
||||
size_t comma = xff.rfind(',', end - 1);
|
||||
size_t start = comma == std::string::npos ? 0 : comma + 1;
|
||||
std::string ip = normalizeIPLiteral(xff.substr(start, end - start));
|
||||
if (!ip.empty()) return ip;
|
||||
if (comma == std::string::npos) break;
|
||||
end = comma;
|
||||
}
|
||||
return normalizeIPLiteral(httpHeader(request, "x-real-ip"));
|
||||
}
|
||||
|
||||
static bool isLoopbackPeer(const std::string& ip) {
|
||||
return ip.rfind("127.", 0) == 0 || ip == "::1" ||
|
||||
ip == "::ffff:127.0.0.1";
|
||||
}
|
||||
|
||||
// Parse Proxy Protocol v2 header and extract real client IP
|
||||
bool parseProxyProtocolV2(SOCKET sock, std::string& clientIP) {
|
||||
// PP2 signature (12 bytes)
|
||||
@@ -448,6 +517,13 @@ private:
|
||||
if (request.find("\r\n\r\n") != std::string::npos) break;
|
||||
}
|
||||
|
||||
// Behind a same-host reverse proxy the TCP peer is 127.0.0.1; recover
|
||||
// the real client address the proxy recorded in the handshake.
|
||||
if (isLoopbackPeer(clientIP)) {
|
||||
std::string forwarded = forwardedClientIP(request);
|
||||
if (!forwarded.empty()) clientIP = forwarded;
|
||||
}
|
||||
|
||||
// Check if WebSocket upgrade
|
||||
if (request.find("Upgrade: websocket") == std::string::npos) {
|
||||
// Regular HTTP request - extract path
|
||||
|
||||
@@ -232,6 +232,26 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
ApplySort(); // 应用记忆的排序偏好
|
||||
}
|
||||
|
||||
// 按客户端编码 (936 或 UTF-8) 解码字节串,并用 LVM_SETITEMTEXTW 写入列表单元格,
|
||||
// 绕开 ANSI→CP_ACP 回转(非中文 ACP 服务端上中文也能正常显示,与窗口管理同法)。
|
||||
static void SetClientTextItem(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
|
||||
{
|
||||
std::wstring w;
|
||||
if (sz && sz[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 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);
|
||||
}
|
||||
|
||||
void CSystemDlg::ShowProcessList(void)
|
||||
{
|
||||
// GetMyBuffer(1) 深拷贝数据,跳过 TOKEN 字节
|
||||
@@ -248,6 +268,10 @@ void CSystemDlg::ShowProcessList(void)
|
||||
const char *szProcessFullPath;
|
||||
DWORD dwOffset = 0;
|
||||
CString str;
|
||||
// 进程名/路径编码:Windows 客户端 A 接口生成,恒为 GBK/936;仅 LNX/MAC 为 UTF-8。
|
||||
// 不能用 GetClientEncoding(它对新客户端返回 CP_UTF8,会把 GBK 进程名解成乱码)。
|
||||
UINT cp = GetProcessListEncoding(m_ContextObject);
|
||||
|
||||
DeleteAllItems();
|
||||
//遍历发送来的每一个字符别忘了他的数据结构啊 Id+进程名+0+完整名+0
|
||||
int i;
|
||||
@@ -258,10 +282,11 @@ void CSystemDlg::ShowProcessList(void)
|
||||
szProcessFullPath = szExeFile + lstrlen(szExeFile) + 1; //完整名就是进程名之后的啦
|
||||
//他的数据结构的构建很巧妙
|
||||
|
||||
m_ControlList.InsertItem(i, arr[0].c_str()); //将得到的数据加入到列表当中
|
||||
m_ControlList.InsertItem(i, ""); // 占位,首列随后用 Unicode 写入
|
||||
str.FormatL("%5u", *PID);
|
||||
m_ControlList.SetItemText(i, 1, str);
|
||||
m_ControlList.SetItemText(i, 2, szProcessFullPath);
|
||||
m_ControlList.SetItemText(i, 1, str); // PID (ASCII)
|
||||
SetClientTextItem(m_ControlList, i, 0, arr[0].c_str(), cp); // 进程名(按客户端编码)
|
||||
SetClientTextItem(m_ControlList, i, 2, szProcessFullPath, cp); // 程序路径(按客户端编码)
|
||||
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
|
||||
// ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
|
||||
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() };
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "stdafx.h"
|
||||
#include "TlsCertificateDlg.h"
|
||||
#include "LangManager.h" // _TR / MessageBoxL
|
||||
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
|
||||
#include "ServerTlsSession.h" // ServerTlsIdentity::Load / IsLoadError
|
||||
#include "common/iniFile.h" // config::GetInt/GetStr/SetInt/SetStr
|
||||
#include <atlconv.h>
|
||||
#include <afxdlgs.h>
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// 构造一个无控件的 DLGTEMPLATE:cdit=0,控件由 OnInitDialog 动态创建。
|
||||
// (与 McpSettingsDlg.cpp / ZstaPickerDlg.cpp 内同名函数一致)
|
||||
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption, short cx, short cy)
|
||||
{
|
||||
out.clear();
|
||||
auto append = [&](const void* p, size_t n) {
|
||||
const BYTE* b = (const BYTE*)p;
|
||||
out.insert(out.end(), b, b + n);
|
||||
};
|
||||
auto appendW = [&](WORD v) {
|
||||
out.push_back((BYTE)(v & 0xFF));
|
||||
out.push_back((BYTE)((v >> 8) & 0xFF));
|
||||
};
|
||||
auto appendWStr = [&](LPCWSTR s) {
|
||||
size_t n = wcslen(s);
|
||||
append(s, (n + 1) * sizeof(WCHAR));
|
||||
};
|
||||
|
||||
DLGTEMPLATE dt = { 0 };
|
||||
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
|
||||
WS_POPUP | WS_CAPTION | WS_SYSMENU;
|
||||
dt.dwExtendedStyle = 0;
|
||||
dt.cdit = 0;
|
||||
dt.x = 0; dt.y = 0;
|
||||
dt.cx = cx; dt.cy = cy;
|
||||
append(&dt, sizeof(dt));
|
||||
appendW(0); // no menu
|
||||
appendW(0); // default dialog class
|
||||
appendWStr(caption); // caption
|
||||
appendW(8); // font point size
|
||||
appendWStr(L"MS Shell Dlg"); // typeface
|
||||
|
||||
while (out.size() % 4) out.push_back(0); // DWORD align
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CTlsCertificateDlg, CDialog)
|
||||
ON_BN_CLICKED(IDC_TLS_CERT_BROWSE, &CTlsCertificateDlg::OnBrowseCert)
|
||||
ON_BN_CLICKED(IDC_TLS_KEY_BROWSE, &CTlsCertificateDlg::OnBrowseKey)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CTlsCertificateDlg::CTlsCertificateDlg(CWnd* parent)
|
||||
: CDialog((LPCTSTR)NULL, parent)
|
||||
{
|
||||
}
|
||||
|
||||
INT_PTR CTlsCertificateDlg::DoModal()
|
||||
{
|
||||
USES_CONVERSION;
|
||||
CString title = _TR("TLS证书");
|
||||
BuildDialogTemplate(m_Template, T2CW(title), 340, 180);
|
||||
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
|
||||
return CDialog::DoModal();
|
||||
}
|
||||
|
||||
BOOL CTlsCertificateDlg::OnInitDialog()
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
CRect cli;
|
||||
GetClientRect(&cli);
|
||||
|
||||
// 占位 rect,真正布局在 LayoutControls 里
|
||||
CRect r0(0, 0, 10, 10);
|
||||
|
||||
m_btnEnable.Create(_TR("启用加密"),
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
|
||||
r0, this, IDC_TLS_ENABLE);
|
||||
m_lblCert.Create(_TR("证书路径"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editCert.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||
r0, this, IDC_TLS_CERT);
|
||||
m_btnCertBrowse.Create(_TR("浏览"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
|
||||
r0, this, IDC_TLS_CERT_BROWSE);
|
||||
m_lblKey.Create(_TR("私钥路径"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
|
||||
m_editKey.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
|
||||
r0, this, IDC_TLS_KEY);
|
||||
m_btnKeyBrowse.Create(_TR("浏览"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
|
||||
r0, this, IDC_TLS_KEY_BROWSE);
|
||||
m_btnOK.Create(_TR("确定"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
|
||||
r0, this, IDOK);
|
||||
m_btnCancel.Create(_TR("取消"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
|
||||
r0, this, IDCANCEL);
|
||||
|
||||
// 子控件继承对话框字体 (DS_SETFONT)
|
||||
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
|
||||
if (hFont) {
|
||||
m_btnEnable.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblCert.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editCert.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnCertBrowse.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_lblKey.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_editKey.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnKeyBrowse.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
m_btnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
// 回填配置
|
||||
int enabled = THIS_CFG.GetInt("tls", "Enabled", 0);
|
||||
std::string cert = THIS_CFG.GetStr("tls", "CertPath", "");
|
||||
std::string key = THIS_CFG.GetStr("tls", "KeyPath", "");
|
||||
|
||||
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
|
||||
m_editCert.SetWindowText(CString(cert.c_str()));
|
||||
m_editKey.SetWindowText(CString(key.c_str()));
|
||||
|
||||
LayoutControls(cli.Width(), cli.Height());
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CTlsCertificateDlg::OnOK()
|
||||
{
|
||||
CString sCert, sKey;
|
||||
m_editCert.GetWindowText(sCert);
|
||||
m_editKey.GetWindowText(sKey);
|
||||
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
|
||||
|
||||
std::string cert = CT2A(sCert);
|
||||
std::string key = CT2A(sKey);
|
||||
|
||||
// 校验:启用加密时必须填写证书/私钥路径
|
||||
if (enabled && cert.empty()) {
|
||||
MessageBoxL("请填写证书路径", "提示", MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
if (enabled && key.empty()) {
|
||||
MessageBoxL("请填写私钥路径", "提示", MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 落盘 [tls] 三键
|
||||
THIS_CFG.SetInt("tls", "Enabled", enabled ? 1 : 0);
|
||||
THIS_CFG.SetStr("tls", "CertPath", cert);
|
||||
THIS_CFG.SetStr("tls", "KeyPath", key);
|
||||
|
||||
// 热重载身份(设计文档 §7.1):开关与证书/私钥路径对新建连接即时生效。
|
||||
// 已知窄竞态:Load 内部 FreeIdentity 与「已取 Cred 副本、尚在握手」的连接存在竞态,
|
||||
// 未来热切换需引用计数 + 惰性释放(见 ServerTlsSession.cpp FreeIdentity 注释),本次不做。
|
||||
ServerTlsIdentity::Load(THIS_CFG);
|
||||
|
||||
if (ServerTlsIdentity::IsLoadError()) {
|
||||
MessageBoxL("证书/私钥加载失败,加密功能未生效。请检查文件路径与格式(证书 PEM、私钥 PKCS#8)。",
|
||||
"提示", MB_ICONWARNING);
|
||||
} else {
|
||||
MessageBox(_TR("TLS 配置已保存。") + _T("\r\n") +
|
||||
_TR("改动对新建连接即时生效。"),
|
||||
_TR("提示"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
void CTlsCertificateDlg::OnBrowseCert()
|
||||
{
|
||||
CFileDialog dlg(TRUE, _T("crt"), nullptr,
|
||||
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
|
||||
_T("Certificate Files (*.crt;*.pem;*.cer;*.cert)|*.crt;*.pem;*.cer;*.cert|All Files (*.*)|*.*||"),
|
||||
this);
|
||||
dlg.m_ofn.lpstrTitle = _T("Select Certificate");
|
||||
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
m_editCert.SetWindowText(dlg.GetPathName());
|
||||
}
|
||||
}
|
||||
|
||||
void CTlsCertificateDlg::OnBrowseKey()
|
||||
{
|
||||
CFileDialog dlg(TRUE, _T("key"), nullptr,
|
||||
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
|
||||
_T("Key Files (*.key;*.pem)|*.key;*.pem|All Files (*.*)|*.*||"),
|
||||
this);
|
||||
dlg.m_ofn.lpstrTitle = _T("Select Private Key");
|
||||
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
m_editKey.SetWindowText(dlg.GetPathName());
|
||||
}
|
||||
}
|
||||
|
||||
void CTlsCertificateDlg::LayoutControls(int cx, int cy)
|
||||
{
|
||||
const int margin = 14;
|
||||
const int labelW = 72;
|
||||
const int rowH = 24;
|
||||
const int gap = 10;
|
||||
const int browseW = 64;
|
||||
const int btnW = 88;
|
||||
const int btnH = 26;
|
||||
|
||||
int y = margin;
|
||||
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
|
||||
y += 30;
|
||||
|
||||
int editW = cx - margin * 2 - labelW - browseW - gap;
|
||||
|
||||
m_lblCert.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editCert.MoveWindow(margin + labelW, y - 2, editW, rowH);
|
||||
m_btnCertBrowse.MoveWindow(cx - margin - browseW, y - 2, browseW, rowH);
|
||||
y += rowH + gap;
|
||||
|
||||
m_lblKey.MoveWindow(margin, y, labelW, rowH);
|
||||
m_editKey.MoveWindow(margin + labelW, y - 2, editW, rowH);
|
||||
m_btnKeyBrowse.MoveWindow(cx - margin - browseW, y - 2, browseW, rowH);
|
||||
y += rowH + gap;
|
||||
|
||||
// 确定 / 取消按钮紧随私钥路径下方
|
||||
m_btnCancel.MoveWindow(cx - margin - btnW, y, btnW, btnH);
|
||||
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, y, btnW, btnH);
|
||||
y += btnH + margin;
|
||||
|
||||
// 收缩对话框到内容实际高度并重新居中,消除底部空白。
|
||||
if (y < cy) {
|
||||
CRect wnd;
|
||||
GetWindowRect(&wnd);
|
||||
SetWindowPos(NULL, 0, 0, wnd.Width(), y + (wnd.Height() - cy),
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
CenterWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <afxcmn.h>
|
||||
#include <vector>
|
||||
|
||||
// 「TLS证书」配置对话框:启用服务端单向 TLS 加密 + 指定证书/私钥路径。
|
||||
// 因 2015Remote.rc 为 UTF-16 编码、不宜手工编辑,参照 CMcpSettingsDlg 用
|
||||
// InitModalIndirect + 内存 DLGTEMPLATE 程序化创建(见 docs/TransportTLS_MFCServer_Design.md §7.1)。
|
||||
class CTlsCertificateDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
explicit CTlsCertificateDlg(CWnd* parent = nullptr);
|
||||
|
||||
virtual INT_PTR DoModal();
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void OnOK();
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
enum CtrlId {
|
||||
IDC_TLS_ENABLE = 2001, // 「启用加密」复选框
|
||||
IDC_TLS_CERT = 2002, // 证书路径编辑框
|
||||
IDC_TLS_CERT_BROWSE = 2003, // 证书路径「浏览...」按钮
|
||||
IDC_TLS_KEY = 2004, // 私钥路径编辑框
|
||||
IDC_TLS_KEY_BROWSE = 2005, // 私钥路径「浏览...」按钮
|
||||
};
|
||||
|
||||
CButton m_btnEnable;
|
||||
CStatic m_lblCert, m_lblKey;
|
||||
CEdit m_editCert, m_editKey;
|
||||
CButton m_btnCertBrowse, m_btnKeyBrowse;
|
||||
CButton m_btnOK, m_btnCancel;
|
||||
|
||||
std::vector<BYTE> m_Template; // 内存 DLGTEMPLATE 字节
|
||||
|
||||
void LayoutControls(int cx, int cy);
|
||||
void OnBrowseCert();
|
||||
void OnBrowseKey();
|
||||
};
|
||||
@@ -134,6 +134,13 @@ void CTriggerSettingsDlg::InitControls()
|
||||
SetWindowText(_TR("触发器设置"));
|
||||
GetDlgItem(IDC_STATIC_TRIGGER_TYPE)->SetWindowText(_TR("触发类型:"));
|
||||
GetDlgItem(IDC_STATIC_TRIGGER_ACTION)->SetWindowText(_TR("执行动作:"));
|
||||
|
||||
// 按钮/分组框文字来自对话框模板,非中文系统上会被系统 ACP 转成 "?",须显式设置
|
||||
SetDlgItemText(IDC_STATIC_TRIGGER_LIST, _TR("已配置的触发器"));
|
||||
SetDlgItemText(IDC_BTN_TRIGGER_ADD, _TR("添加 >>"));
|
||||
SetDlgItemText(IDC_BTN_TRIGGER_REMOVE, _TR("<< 移除"));
|
||||
SetDlgItemText(IDC_BTN_SAVE, _TR("保存(&S)"));
|
||||
SetDlgItemText(IDOK, _TR("关闭"));
|
||||
}
|
||||
|
||||
void CTriggerSettingsDlg::LoadPluginsToList()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
// 程序版本号 [建议格式: X.Y.Z]
|
||||
// 影响:关于对话框、标题栏
|
||||
#define BRAND_VERSION "1.4.0"
|
||||
#define BRAND_VERSION "1.4.1"
|
||||
|
||||
// 启动画面名称 [建议大写,更有 Logo 感]
|
||||
// 影响:启动画面 Logo 文字(大号艺术字体渲染)
|
||||
@@ -172,6 +172,7 @@
|
||||
#define HIDE_MENU_PLUGIN_REQUEST 0 // 插件请求
|
||||
#define HIDE_MENU_FRPS_FOR_SUB 0 // 下级FRP
|
||||
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
|
||||
#define HIDE_MENU_TLS_CERTIFICATE 0 // TLS证书
|
||||
|
||||
// --- 扩展菜单 - 语言子菜单 ---
|
||||
#define HIDE_MENU_CHANGE_LANG 0 // 更改语言
|
||||
|
||||
+849
-116
File diff suppressed because it is too large.
Load diff
@@ -5,6 +5,8 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <initializer_list>
|
||||
#include <utility>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
@@ -68,12 +70,20 @@ struct WebClient {
|
||||
std::string token;
|
||||
std::string username;
|
||||
std::string role; // "admin" | "viewer"
|
||||
uint64_t watch_device_id; // 0 = not watching any device
|
||||
// 当前这个 ws 订阅的设备集合。屏幕墙允许多路复用;旧前端仍按单设备语义使用
|
||||
// (connect 替换整个集合)。
|
||||
std::set<uint64_t> watch_device_ids;
|
||||
// 每台设备的 keyframe 请求节流时间戳(秒),防止前端重试打爆服务端/客户端。
|
||||
std::map<uint64_t, uint64_t> keyframe_req_at;
|
||||
uint64_t connected_at;
|
||||
uint64_t last_activity; // Last message/pong received (for heartbeat timeout)
|
||||
std::string client_ip;
|
||||
// Per-connection audit de-duplication: probes an attacker can repeat in a
|
||||
// tight loop are recorded once per connection instead of flooding the ring.
|
||||
bool salt_probe_logged = false;
|
||||
bool login_throttle_logged = false;
|
||||
|
||||
WebClient() : watch_device_id(0), connected_at(0), last_activity(0) {}
|
||||
WebClient() : connected_at(0), last_activity(0) {}
|
||||
};
|
||||
|
||||
// Login attempt tracking for rate limiting
|
||||
@@ -160,6 +170,9 @@ public:
|
||||
// Cursor change notification (called from ScreenSpyDlg)
|
||||
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
|
||||
|
||||
// 远程剪贴板文本回传通知(called from ScreenSpyDlg,payload 为 UTF-8)
|
||||
void NotifyClipboardText(uint64_t device_id, const std::string& text);
|
||||
|
||||
// Get count of web clients watching a device
|
||||
int GetWebClientCount(uint64_t device_id);
|
||||
|
||||
@@ -182,13 +195,24 @@ private:
|
||||
void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip);
|
||||
void HandleGetSalt(void* ws_ptr, const std::string& msg);
|
||||
void HandleGetDevices(void* ws_ptr, const std::string& token);
|
||||
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
|
||||
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
|
||||
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req = "", bool multi_watch = false);
|
||||
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0,
|
||||
const std::string& req = "", bool multi_watch = false);
|
||||
void HandleKeyframe(void* ws_ptr, const std::string& token, uint64_t device_id,
|
||||
const std::string& req);
|
||||
void HandlePing(void* ws_ptr, const std::string& token);
|
||||
void HandleMouse(void* ws_ptr, const std::string& msg);
|
||||
void HandleKey(void* ws_ptr, const std::string& msg);
|
||||
void HandleRdpReset(void* ws_ptr, const std::string& token);
|
||||
void HandleAudioToggle(void* ws_ptr, const std::string& token);
|
||||
void HandleClipboardSet(void* ws_ptr, const std::string& msg);
|
||||
void HandleClipboardGet(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
void HandleRdpReset(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
void HandleAudioToggle(void* ws_ptr, const std::string& token, uint64_t device_id = 0);
|
||||
|
||||
// 输入类命令的设备解析:优先用显式 id(必须在 watch 集合内);否则当集合恰好
|
||||
// 只有一台设备时回退到它(兼容不带 id 的旧前端)。多设备且无 id 时返回 0(丢弃),
|
||||
// 绝不能回退到任意集合成员,否则会变成跨设备输入注入。
|
||||
uint64_t ResolveWatchDevice(void* ws_ptr, uint64_t requested_device_id);
|
||||
|
||||
// Token management
|
||||
std::string GenerateToken(const std::string& username, const std::string& role);
|
||||
@@ -221,6 +245,15 @@ private:
|
||||
void HandleListUsers(void* ws_ptr, const std::string& token);
|
||||
void HandleGetGroups(void* ws_ptr, const std::string& token);
|
||||
|
||||
// Security audit trail: in-memory ring of the last 1000 events since
|
||||
// startup (see WebService.cpp). query_audit is admin-only.
|
||||
void HandleQueryAudit(void* ws_ptr, const std::string& msg);
|
||||
std::string ClientIP(void* ws_ptr);
|
||||
void AuditWeb(const std::string& event, bool warn,
|
||||
const std::string& actor, const std::string& role,
|
||||
const std::string& ip, void* ws_ptr, uint64_t device_id,
|
||||
std::initializer_list<std::pair<const char*, std::string>> extra = {});
|
||||
|
||||
// Send to WebSocket
|
||||
void SendText(void* ws_ptr, const std::string& text);
|
||||
void SendBinary(void* ws_ptr, const uint8_t* data, size_t len);
|
||||
@@ -241,6 +274,10 @@ private:
|
||||
static const int HEARTBEAT_INTERVAL_SEC = 30; // Send ping every 30 seconds
|
||||
static const int HEARTBEAT_TIMEOUT_SEC = 90; // Disconnect if no activity for 90 seconds
|
||||
|
||||
// 每个 ws 最多订阅的设备数(屏幕墙最多 25 个 tile,留余量)。防止异常客户端
|
||||
// 订阅数百台设备拖慢每一帧的广播循环。
|
||||
static constexpr size_t kMaxWatchPerClient = 32;
|
||||
|
||||
// Heartbeat thread function
|
||||
void HeartbeatThread();
|
||||
|
||||
@@ -366,8 +403,12 @@ private:
|
||||
std::set<uint64_t> m_TermPending; // 已发 COMMAND_SHELL 待响应
|
||||
std::mutex m_TermMutex;
|
||||
|
||||
// 内部清理(已持锁版本)
|
||||
void CloseTermSessionLocked(uint64_t device_id);
|
||||
// 内部清理(已持锁版本)。reason/actor/role/ip 仅用于审计日志;actor 为空时
|
||||
// 尝试从 m_Clients 反查(ws 已注销时需由调用方传入)。
|
||||
void CloseTermSessionLocked(uint64_t device_id, const char* reason = "ws_close",
|
||||
const std::string& actor = std::string(),
|
||||
const std::string& role = std::string(),
|
||||
const std::string& ip = std::string());
|
||||
};
|
||||
|
||||
// Global accessor
|
||||
|
||||
@@ -22,6 +22,7 @@ enum {
|
||||
ONLINELIST_STARTTIME,
|
||||
ONLINELIST_CAPABILITIES, // 客户端能力位
|
||||
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
||||
ONLINELIST_TLS, // TLS 传输启用标志 (0/1)
|
||||
ONLINELIST_MAX,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "InputDlg.h"
|
||||
#include <windows.h>
|
||||
#include <Shlwapi.h>
|
||||
#include <string>
|
||||
|
||||
#ifdef _DEBUG
|
||||
#define new DEBUG_NEW
|
||||
@@ -157,6 +158,15 @@ BOOL CFileManagerDlg::OnInitDialog()
|
||||
SetDlgItemText(IDC_STATIC_FILE_WINOS_SEARCH, _TR("搜索文件名:"));
|
||||
SetDlgItemText(IDC_STATIC_FILE_WINOS_PATH, _TR("路径:"));
|
||||
|
||||
// 多语言翻译 - 按钮/复选框(模板文字在非中文系统会被系统 ACP 转成 "?",须显式设置)
|
||||
SetDlgItemText(IDC_CK_SUBFORDLE, _TR("包含子文件夹"));
|
||||
SetDlgItemText(IDC_BTN_SEARCH, _TR("开始搜索"));
|
||||
SetDlgItemText(ID_SEARCH_STOP, _TR("停止搜索"));
|
||||
SetDlgItemText(ID_SEARCH_RESULT, _TR("搜索栏"));
|
||||
SetDlgItemText(IDT_REMOTE_PREV, _TR("上一层"));
|
||||
SetDlgItemText(IDT_REMOTE_VIEW, _TR("切换显示"));
|
||||
SetDlgItemText(IDT_REMOTE_STOP, _TR("取消"));
|
||||
|
||||
RECT rect;
|
||||
GetClientRect(&rect);
|
||||
|
||||
@@ -809,8 +819,32 @@ void CFileManagerDlg::OnclickListSearch(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
|
||||
int GetIconIndex(LPCTSTR lpFileName, DWORD dwFileAttributes);
|
||||
|
||||
// 远程文件名/路径编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA)=GBK/936,
|
||||
// LNX/MAC 文件系统天然 UTF-8(同 McpServer.cpp::BuildListFiles)。这里用 LVM_SETITEMTEXTW
|
||||
// 直接写宽字符串,绕开 ANSI→CP_ACP 回转,即使服务端运行在非中文 ACP 上中文名也能正常显示。
|
||||
static void SetFileTextItem(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
|
||||
{
|
||||
std::wstring w;
|
||||
if (sz && sz[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 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);
|
||||
}
|
||||
|
||||
void CFileManagerDlg::FixedRemoteFileList(BYTE* pbBuffer, DWORD dwBufferLen)
|
||||
{
|
||||
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
|
||||
|
||||
// 重建标题
|
||||
m_list_remote.DeleteAllItems();
|
||||
int nItemIndex = 0;
|
||||
@@ -846,7 +880,8 @@ void CFileManagerDlg::FixedRemoteFileList(BYTE* pbBuffer, DWORD dwBufferLen)
|
||||
|
||||
if (bIsInsert) {
|
||||
int iicon = GetIconIndex(pszFileName, nType);
|
||||
nItem = m_list_remote.InsertItem(nItemIndex++, pszFileName, iicon);
|
||||
nItem = m_list_remote.InsertItem(nItemIndex++, _T(""), iicon);
|
||||
SetFileTextItem(m_list_remote, nItem, 0, pszFileName, cp);
|
||||
m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
|
||||
SHFILEINFO sfi = {};
|
||||
SHGetFileInfo(pszFileName, FILE_ATTRIBUTE_NORMAL | nType, &sfi, sizeof(SHFILEINFO), SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES);
|
||||
@@ -2412,6 +2447,9 @@ void CFileManagerDlg::SearchEnd()
|
||||
|
||||
void CFileManagerDlg::FixedRemoteSearchFileList(BYTE* pbBuffer, DWORD dwBufferLen)
|
||||
{
|
||||
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
|
||||
|
||||
byte* pList = pbBuffer + 1;
|
||||
int nItemIndex = 0;
|
||||
for (byte* pBase = pList; (int)(pList - pBase) < (int)(dwBufferLen - 1);) {
|
||||
@@ -2430,7 +2468,8 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE* pbBuffer, DWORD dwBufferLe
|
||||
int nPos = csFilePath.ReverseFind(_T('\\'));
|
||||
csFileFullName = csFilePath.Right(csFilePath.GetLength() - nPos - 1);// 获取文件全名,包括文件名和扩展名
|
||||
|
||||
nItem = m_list_remote_search.InsertItem(nItemIndex++, csFileFullName, GetIconIndex(pszFileName, nType));
|
||||
nItem = m_list_remote_search.InsertItem(nItemIndex++, _T(""), GetIconIndex(pszFileName, nType));
|
||||
SetFileTextItem(m_list_remote_search, nItem, 0, csFileFullName, cp);
|
||||
m_list_remote_search.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
|
||||
|
||||
pList += (lstrlen(pszFileName) + 1) * sizeof(TCHAR);
|
||||
@@ -2446,7 +2485,7 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE* pbBuffer, DWORD dwBufferLe
|
||||
m_list_remote_search.SetItemText(nItem, 2, time.FormatL(_T("%Y-%m-%d %H:%M")));
|
||||
|
||||
PathRemoveFileSpec(pszFileName); //去除文件名获取文件路径
|
||||
m_list_remote_search.SetItemText(nItem, 3, pszFileName);
|
||||
SetFileTextItem(m_list_remote_search, nItem, 3, pszFileName, cp);
|
||||
|
||||
pList += 16;
|
||||
}
|
||||
@@ -2482,6 +2521,9 @@ void CFileManagerDlg::ShowSearchPlugList()
|
||||
char* Filename;
|
||||
char* Pathname;
|
||||
|
||||
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
|
||||
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
|
||||
|
||||
DWORD dwOffset = 0;
|
||||
m_list_remote_search.DeleteAllItems();
|
||||
int i = 0;
|
||||
@@ -2489,13 +2531,13 @@ void CFileManagerDlg::ShowSearchPlugList()
|
||||
Filename = lpBuffer + dwOffset;
|
||||
Pathname = Filename + strlen(Filename) + 1;
|
||||
CString CS_tem;
|
||||
CS_tem = Filename;
|
||||
m_list_remote_search.InsertItem(i, CS_tem, 0);
|
||||
m_list_remote_search.InsertItem(i, _T(""), 0);
|
||||
SetFileTextItem(m_list_remote_search, i, 0, Filename, cp);
|
||||
CS_tem = Pathname;
|
||||
int index = CS_tem.ReverseFind('\\');
|
||||
if (index >= 0)
|
||||
CS_tem = CS_tem.Left(index);
|
||||
m_list_remote_search.SetItemText(i,3, CS_tem);
|
||||
SetFileTextItem(m_list_remote_search, i, 3, CS_tem, cp);
|
||||
dwOffset += strlen(Filename) + strlen(Pathname) + 2;
|
||||
}
|
||||
m_list_remote_search.EnableWindow(TRUE);
|
||||
|
||||
@@ -306,6 +306,7 @@ Web
|
||||
; ============================================
|
||||
发送消息=Nachricht senden
|
||||
结束交谈=Chat beenden
|
||||
确定开始远程交谈吗?=Remote-Chat starten?
|
||||
锁定屏幕\n屏蔽功能键=Bildschirm sperren\nTasten blockieren
|
||||
解除锁定=Entsperren
|
||||
|
||||
@@ -1991,7 +1992,7 @@ MCP
|
||||
端口需为 1-65535 的数字=Port muss eine Zahl zwischen 1 und 65535 sein
|
||||
Token 不能为空=Token darf nicht leer sein
|
||||
MCP 设置已保存。=MCP-Einstellungen gespeichert.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen werden nach Neustart wirksam.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Aenderungen an Aktivierung/Port/Bindeadresse/Token/Nur-Lesen/Whitelist/persistentem Terminal/Fernsteuerung/Dateiuebertragung/Verwaltungsoperationen/Maschinenverwaltung/gefaehrlichen Maschinenoperationen werden nach Neustart wirksam.
|
||||
只读模式(禁命令执行)=Nur-Lesen-Modus (Befehlsausfuehrung deaktiviert)
|
||||
命令白名单=Befehls-Whitelist
|
||||
MCP命令执行=MCP-Befehlsausfuehrung
|
||||
@@ -2005,6 +2006,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=Fernsteuerung aktivieren (KI steuert den Desktop)
|
||||
启用文件传输(download_file)=Dateiuebertragung aktivieren (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Verwaltungsoperationen aktivieren (Prozess/Fenster/Dienst/Energie)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Maschinenverwaltung aktivieren (Prozess/Fenster/Dienst/Aufgabe/Netzwerkstatus)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Gefaehrliche Maschinenoperationen aktivieren (Prozess einfrieren/auftauen, Dienst stoppen/neu starten)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Gefaehrliche Maschinenoperationen setzen die Maschinenverwaltung voraus. Aktivieren Sie zuerst die Maschinenverwaltung.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Schreiboperationen der Maschinenverwaltung erfordern deaktivierten Nur-Lesen-Modus; Einfrieren/Auftauen von Prozessen und Stoppen/Neustarten von Diensten erfordern zusaetzlich gefaehrliche Maschinenoperationen.
|
||||
第 %d 个钱包地址不合法!=Wallet-Adresse #%d ist ungueltig!
|
||||
%d 新连接\r\n=%d Neue Verbindung\r\n
|
||||
%d 本地连接断开\r\n=%d Lokale Verbindung geschlossen\r\n
|
||||
@@ -2068,3 +2073,34 @@ JPEG
|
||||
%s · 约 %.2f MB/s=%s · ca. %.2f MB/s
|
||||
%s · 不受限速(受网络带宽约束)=%s · Unbegrenzt (durch Netzwerkbandbreite begrenzt)
|
||||
文件传输速度已保存。上传速度重启主控端后生效;下载速度已推送到在线客户端,新连接的主机也会自动获取。=Uebertragungsgeschwindigkeit gespeichert. Upload-Geschwindigkeit wird nach Neustart des Masters wirksam; Download-Geschwindigkeit wurde an Online-Clients gesendet, neu verbundene Hosts erhalten sie automatisch.
|
||||
|
||||
; ===== TLS =====
|
||||
TLS证书(&T)...=TLS-Zertifikat(&T)...
|
||||
TLS证书=TLS-Zertifikat
|
||||
启用加密=Verschluesselung aktivieren
|
||||
证书路径=Zertifikatspfad
|
||||
私钥路径=Pfad zum privaten Schluessel
|
||||
请填写证书路径=Bitte Zertifikatspfad angeben
|
||||
请填写私钥路径=Bitte Pfad zum privaten Schluessel angeben
|
||||
TLS 配置已保存。=TLS-Einstellungen gespeichert.
|
||||
改动对新建连接即时生效。=Aenderungen gelten sofort fuer neue Verbindungen.
|
||||
证书/私钥加载失败,加密功能未生效。请检查文件路径与格式(证书 PEM、私钥 PKCS#8)。=Zertifikat/privater Schluessel konnte nicht geladen werden; Verschluesselung ist inaktiv. Bitte Pfad und Format pruefen (PEM-Zertifikat, PKCS#8-Schluessel).
|
||||
TLS传输=TLS-Transport
|
||||
取消TLS=TLS deaktivieren
|
||||
确定对选中主机启用 TLS 传输吗?(客户端下次启动生效)=TLS-Transport fuer die ausgewaehlten Hosts aktivieren? (wirkt nach Client-Neustart)
|
||||
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=TLS-Transport fuer die ausgewaehlten Hosts deaktivieren? (wirkt nach Client-Neustart)
|
||||
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server-TLS-Zertifikat ist nicht aktiviert; TLS-Transport der Clients kann nicht umgeschaltet werden
|
||||
读取服务端 TLS 证书失败,无法下发=Server-TLS-Zertifikat konnte nicht gelesen werden; Verteilung nicht moeglich
|
||||
白名单=Whitelist
|
||||
确定将选中日志中的 IP 加入白名单吗?=IP aus den ausgewaehlten Log-Eintraegen zur Whitelist hinzufuegen?
|
||||
已将 %d 个 IP 加入白名单=%d IP(s) zur Whitelist hinzugefuegt
|
||||
%d 个 IP 已在白名单中=%d IP(s) bereits in der Whitelist
|
||||
%d 个 IP 已封禁,无法加入白名单=%d IP(s) sind gesperrt und koennen nicht zur Whitelist hinzugefuegt werden
|
||||
%s 文件句柄无效=%s ungueltiges Datei-Handle
|
||||
没有选中任何文件=Keine Dateien ausgewaehlt
|
||||
V2传输已启动,共 %d 个文件 -> %s=V2-Uebertragung gestartet, %d Datei(en) -> %s
|
||||
V2下载请求已发送,共 %d 个项目=V2-Download-Anfrage gesendet, %d Element(e)
|
||||
找不到主连接: clientID %llu=Hauptverbindung nicht gefunden: clientID=%llu
|
||||
(已过期,续期从今天起算)= (abgelaufen; Verlaengerung ab heute)
|
||||
(原授权已过期,续期从今天起算)=(Urspruengliche Lizenz abgelaufen; Verlaengerung ab heute)
|
||||
子目录=SubDir
|
||||
@@ -99,6 +99,8 @@ RTT=RTT
|
||||
即时消息=Message
|
||||
虚拟屏幕=Virtual Screen
|
||||
隐私屏幕=Privacy Screen
|
||||
实验功能=Experimental Features
|
||||
远程画板=Remote Drawing Board
|
||||
代理服务=Proxy Service
|
||||
注入设置=Inject Config
|
||||
创建计划任务=Scheduled Task
|
||||
@@ -121,6 +123,7 @@ RTT=RTT
|
||||
|
||||
; ============================================
|
||||
; Language Selection Dialog
|
||||
切换语言=Switch Language
|
||||
; ============================================
|
||||
选择语言 / Select Language=Select Language
|
||||
语言:=Language:
|
||||
@@ -306,6 +309,7 @@ Web
|
||||
; ============================================
|
||||
发送消息=Send Msg
|
||||
结束交谈=End Chat
|
||||
确定开始远程交谈吗?=Start remote chat?
|
||||
锁定屏幕\n屏蔽功能键=Lock Screen\nBlock Keys
|
||||
解除锁定=Unlock
|
||||
|
||||
@@ -1397,6 +1401,7 @@ RDP
|
||||
C2C 文件传输=C2C File Transfer
|
||||
续传文件 (V2)=Resume File Upload (V2)
|
||||
发送文件 (V2)=Send File (V2)
|
||||
接收文件 (V2)=Receive File (V2)
|
||||
发送文件(%d/%d): %.2f%%=Send Files(%d/%d): %.2f%%
|
||||
接收文件(%d/%d): %.2f%%=Recv Files(%d/%d): %.2f%%
|
||||
[远程右键复制, 本地 Ctrl+V 粘贴]= [Remote Right-click Copy, Local Ctrl+V Paste]
|
||||
@@ -1992,7 +1997,7 @@ MCP
|
||||
端口需为 1-65535 的数字=Port must be a number between 1 and 65535
|
||||
Token 不能为空=Token cannot be empty
|
||||
MCP 设置已保存。=MCP settings saved.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations take effect after restart.
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=Changes to enable/port/bind address/token/read-only/whitelist/persistent terminal/remote control/file transfer/management operations/host management/dangerous host operations take effect after restart.
|
||||
只读模式(禁命令执行)=Read-only mode (disable command execution)
|
||||
命令白名单=Command whitelist
|
||||
MCP命令执行=MCP command execution
|
||||
@@ -2006,6 +2011,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=Enable remote control (AI controls the desktop)
|
||||
启用文件传输(download_file)=Enable file transfer (download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=Enable management operations (process/window/service/power)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=Enable host management (processes/windows/services/tasks/network state)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=Enable dangerous host operations (freeze/thaw process, stop/restart service)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=Dangerous host operations depend on host management. Enable host management first.
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=Host management write operations require read-only mode to be off; freezing/thawing processes and stopping/restarting services also require dangerous host operations.
|
||||
第 %d 个钱包地址不合法!=Wallet address #%d is invalid!
|
||||
%d 新连接\r\n=%d New connection\r\n
|
||||
%d 本地连接断开\r\n=%d Local connection closed\r\n
|
||||
@@ -2079,3 +2088,34 @@ JPEG
|
||||
连接数:%d=Connections: %d
|
||||
强制断开=Force Disconnect
|
||||
远程交谈=Talk
|
||||
|
||||
; ===== TLS =====
|
||||
TLS证书(&T)...=TLS Certificate(&T)...
|
||||
TLS证书=TLS Certificate
|
||||
启用加密=Enable Encryption
|
||||
证书路径=Certificate Path
|
||||
私钥路径=Private Key Path
|
||||
请填写证书路径=Please specify the certificate path
|
||||
请填写私钥路径=Please specify the private key path
|
||||
TLS 配置已保存。=TLS settings saved.
|
||||
改动对新建连接即时生效。=Changes take effect immediately for new connections.
|
||||
证书/私钥加载失败,加密功能未生效。请检查文件路径与格式(证书 PEM、私钥 PKCS#8)。=Failed to load certificate/private key; encryption is not active. Check the file path and format (PEM certificate, PKCS#8 private key).
|
||||
TLS传输=TLS Transport
|
||||
取消TLS=Disable TLS
|
||||
确定对选中主机启用 TLS 传输吗?(客户端下次启动生效)=Enable TLS transport for the selected host? (takes effect after the client restarts)
|
||||
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=Disable TLS transport for the selected host? (takes effect after the client restarts)
|
||||
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=Server TLS certificate is not enabled; cannot toggle client TLS transport
|
||||
读取服务端 TLS 证书失败,无法下发=Failed to read the server TLS certificate; cannot distribute
|
||||
白名单=Whitelist
|
||||
确定将选中日志中的 IP 加入白名单吗?=Add the IP from the selected log entries to the whitelist?
|
||||
已将 %d 个 IP 加入白名单=Added %d IP(s) to the whitelist
|
||||
%d 个 IP 已在白名单中=%d IP(s) already in the whitelist
|
||||
%d 个 IP 已封禁,无法加入白名单=%d IP(s) are banned and cannot be added to the whitelist
|
||||
%s 文件句柄无效=%s invalid file handle
|
||||
没有选中任何文件=No files selected
|
||||
V2传输已启动,共 %d 个文件 -> %s=V2 transfer started, %d file(s) -> %s
|
||||
V2下载请求已发送,共 %d 个项目=V2 download request sent, %d item(s)
|
||||
找不到主连接: clientID %llu=Main connection not found: clientID=%llu
|
||||
(已过期,续期从今天起算)= (expired; renewal starts today)
|
||||
(原授权已过期,续期从今天起算)=(Original license expired; renewal starts today)
|
||||
子目录=SubDir
|
||||
@@ -99,6 +99,8 @@ RTT=RTT
|
||||
即时消息=即時訊息
|
||||
虚拟屏幕=虛擬桌面
|
||||
隐私屏幕=隱私螢幕
|
||||
实验功能=實驗功能
|
||||
远程画板=遠端繪圖板
|
||||
代理服务=代理服務
|
||||
注入设置=注入設定
|
||||
创建计划任务=建立排程工作
|
||||
@@ -121,6 +123,7 @@ RTT=RTT
|
||||
|
||||
; ============================================
|
||||
; Language Selection Dialog
|
||||
切換語言=切換語言
|
||||
; ============================================
|
||||
选择语言 / Select Language=選擇語言
|
||||
语言:=語言:
|
||||
@@ -306,6 +309,7 @@ Web
|
||||
; ============================================
|
||||
发送消息=傳送訊息
|
||||
结束交谈=結束交談
|
||||
确定开始远程交谈吗?=確定開始遠程交談嗎?
|
||||
锁定屏幕\n屏蔽功能键=鎖定螢幕\n屏蔽功能鍵
|
||||
解除锁定=解除鎖定
|
||||
|
||||
@@ -1393,6 +1397,7 @@ RDP
|
||||
C2C 文件传输=C2C 檔案傳輸
|
||||
续传文件 (V2)=續傳檔案 (V2)
|
||||
发送文件 (V2)=傳送檔案 (V2)
|
||||
接收文件 (V2)=接收檔案 (V2)
|
||||
发送文件(%d/%d): %.2f%%=发送文件(%d/%d): %.2f%%
|
||||
接收文件(%d/%d): %.2f%%=接收文件(%d/%d): %.2f%%
|
||||
[远程右键复制, 本地 Ctrl+V 粘贴]= [遠端右鍵複製, 本地 Ctrl+V 貼上]
|
||||
@@ -1983,7 +1988,7 @@ MCP
|
||||
端口需为 1-65535 的数字=連接埠需為 1-65535 的數字
|
||||
Token 不能为空=Token 不能為空
|
||||
MCP 设置已保存。=MCP 設定已儲存。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作的變更需重新啟動程式後生效。
|
||||
启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作/主机管理/危险主机操作的改动需重启程序生效。=啟用/連接埠/綁定位址/Token/唯讀/白名單/持久終端/遠端控制/檔案傳輸/管理操作/主機管理/危險主機操作的變更需重新啟動程式後生效。
|
||||
只读模式(禁命令执行)=唯讀模式(禁命令執行)
|
||||
命令白名单=命令白名單
|
||||
MCP命令执行=MCP命令執行
|
||||
@@ -1997,6 +2002,10 @@ Token=Token
|
||||
启用远程控制(AI 操控桌面)=啟用遠端控制(AI 操控桌面)
|
||||
启用文件传输(download_file)=啟用檔案傳輸(download_file)
|
||||
启用管理操作(进程/窗口/服务/电源)=啟用管理操作(程序/視窗/服務/電源)
|
||||
启用主机管理(进程/窗口/服务/任务/网络状态)=啟用主機管理(處理程序/視窗/服務/排程工作/網路狀態)
|
||||
启用危险主机操作(冻结/解冻进程、停止/重启服务)=啟用危險主機操作(凍結/解凍處理程序、停止/重新啟動服務)
|
||||
危险主机操作依附于主机管理:请先启用主机管理。=危險主機操作依附於主機管理:請先啟用主機管理。
|
||||
主机管理的写操作需只读模式关闭;冻结/解冻进程、停止/重启服务还需启用危险主机操作。=主機管理的寫入操作需關閉唯讀模式;凍結/解凍處理程序、停止/重新啟動服務還需啟用危險主機操作。
|
||||
第 %d 个钱包地址不合法!=第 %d 個錢包位址不合法!
|
||||
%d 新连接\r\n=%d 新連線\r\n
|
||||
%d 本地连接断开\r\n=%d 本機連線斷開\r\n
|
||||
@@ -2070,3 +2079,34 @@ JPEG
|
||||
连接数:%d=連線數:%d
|
||||
强制断开=強制中斷
|
||||
远程交谈=遠程交談
|
||||
|
||||
; ===== TLS =====
|
||||
TLS证书(&T)...=TLS 憑證(&T)...
|
||||
TLS证书=TLS 憑證
|
||||
启用加密=啟用加密
|
||||
证书路径=憑證路徑
|
||||
私钥路径=私鑰路徑
|
||||
请填写证书路径=請填寫憑證路徑
|
||||
请填写私钥路径=請填寫私鑰路徑
|
||||
TLS 配置已保存。=TLS 設定已儲存。
|
||||
改动对新建连接即时生效。=變更對新連線即時生效。
|
||||
证书/私钥加载失败,加密功能未生效。请检查文件路径与格式(证书 PEM、私钥 PKCS#8)。=憑證/私鑰載入失敗,加密功能未生效。請檢查檔案路徑與格式(憑證 PEM、私鑰 PKCS#8)。
|
||||
TLS传输=TLS 傳輸
|
||||
取消TLS=停用 TLS
|
||||
确定对选中主机启用 TLS 传输吗?(客户端下次启动生效)=確定對選取主機啟用 TLS 傳輸嗎?(用戶端下次啟動生效)
|
||||
确定取消选中主机的 TLS 传输吗?(客户端下次启动生效)=確定取消選取主機的 TLS 傳輸嗎?(用戶端下次啟動生效)
|
||||
服务端未启用 TLS 证书,无法切换客户端 TLS 传输=伺服器未啟用 TLS 憑證,無法切換用戶端 TLS 傳輸
|
||||
读取服务端 TLS 证书失败,无法下发=讀取伺服器 TLS 憑證失敗,無法下發
|
||||
白名单=白名單
|
||||
确定将选中日志中的 IP 加入白名单吗?=確定將選取日誌中的 IP 加入白名單嗎?
|
||||
已将 %d 个 IP 加入白名单=已將 %d 個 IP 加入白名單
|
||||
%d 个 IP 已在白名单中=%d 個 IP 已在白名單中
|
||||
%d 个 IP 已封禁,无法加入白名单=%d 個 IP 已封禁,無法加入白名單
|
||||
%s 文件句柄无效=%s 檔案控制代碼無效
|
||||
没有选中任何文件=沒有選取任何檔案
|
||||
V2传输已启动,共 %d 个文件 -> %s=V2 傳輸已啟動,共 %d 個檔案 -> %s
|
||||
V2下载请求已发送,共 %d 个项目=V2 下載請求已傳送,共 %d 個項目
|
||||
找不到主连接: clientID %llu=找不到主連線: clientID=%llu
|
||||
(已过期,续期从今天起算)= (已過期,續期從今天起算)
|
||||
(原授权已过期,续期从今天起算)=(原授權已過期,續期從今天起算)
|
||||
子目录=子目录
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -48,9 +48,6 @@
|
||||
#define IDI_ICON_STRING 163
|
||||
#define IDD_DIALOG_SERVICES 164
|
||||
#define IDR_MENU_SERVICES 165
|
||||
#define IDD_DIALOG_CONNECTIONS 404
|
||||
#define IDR_MENU_CONNECTIONS 405
|
||||
#define IDI_CONNECTION 406
|
||||
#define IDD_DIALOG_VIDEO 166
|
||||
#define IDC_CURSOR2 168
|
||||
#define IDC_CURSOR1 171
|
||||
@@ -266,7 +263,6 @@
|
||||
#define IDR_WEB_XTERM_CSS 383
|
||||
#define IDR_WEB_XTERM_FIT_JS 384
|
||||
#define IDR_WEB_INDEX_HTML 385
|
||||
#define IDR_WEB_OPUS_DECODER_JS 401
|
||||
#define IDB_BITMAP_COMPRESS 386
|
||||
#define IDB_BITMAP_UNCOMPRESS 387
|
||||
#define IDB_BITMAP9 388
|
||||
@@ -284,9 +280,21 @@
|
||||
#define IDB_BITMAP_HISTORY_ACTIVITY 398
|
||||
#define IDB_BITMAP_VIEW_WND 399
|
||||
#define IDB_BITMAP_PROXY_AUTORUN 400
|
||||
#define IDR_WEB_OPUS_DECODER_JS 401
|
||||
#define IDD_DIALOG_SHARE_HOST 402
|
||||
#define IDD_FILE_SPEED 403
|
||||
#define IDD_DIALOG_CONNECTIONS 404
|
||||
#define IDR_MENU_CONNECTIONS 405
|
||||
#define IDI_CONNECTION 406
|
||||
#define IDB_BITMAP_FILE_MANAGER 407
|
||||
#define IDB_BITMAP_PRIVACY_SHIELD 408
|
||||
#define IDB_BITMAP_DRAWING_BOARD 409
|
||||
#define IDB_BITMAP_TLS_ENABLE 410
|
||||
#define IDB_BITMAP_TLS_DISABLE 411
|
||||
#define IDB_BITMAP_DESKTOP_QUALITY 412
|
||||
#define IDB_BITMAP_PREVIEW_QUALITY 413
|
||||
#define IDB_BITMAP_FILE_SPEED 414
|
||||
#define IDB_BITMAP_DISCLAIMER 415
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
#define IDC_STATIC_TIPS 1002
|
||||
@@ -360,8 +368,6 @@
|
||||
#define IDI_SYSTEM 2035
|
||||
#define IDC_DOT 2036
|
||||
#define IDD_AUDIO 2038
|
||||
#define IDD_QUALITY_LEVELS 3000
|
||||
#define IDD_PREVIEW_LEVELS 3001
|
||||
#define IDI_AUDIO 2039
|
||||
#define IDC_LIST_REMOTE 2040
|
||||
#define IDC_LIST_LOCAL 2041
|
||||
@@ -525,6 +531,7 @@
|
||||
#define IDC_PROGRESS_FILESEND 2214
|
||||
#define IDC_STATIC_PAYLOAD3 2214
|
||||
#define IDC_STATIC_CURRENTINDEX 2215
|
||||
#define IDC_STATIC_LOCATION 2215
|
||||
#define IDC_STATIC_CURRENTPERCENT 2216
|
||||
#define IDC_EDIT_INSTALL_DIR 2216
|
||||
#define IDC_EDIT_INSTALL_NAME 2217
|
||||
@@ -775,6 +782,52 @@
|
||||
#define IDC_STATIC_TRIGGER_TYPE 2544
|
||||
#define IDC_STATIC_TRIGGER_ACTION 2545
|
||||
#define IDC_EDIT_LOG 2546
|
||||
#define IDC_QLEVEL_FPS 2547
|
||||
#define IDC_QLEVEL_WIDTH 2553
|
||||
#define IDC_QLEVEL_ALGO 2559
|
||||
#define IDC_QLEVEL_BITRATE 2565
|
||||
#define IDC_QLEVEL_RESET 2571
|
||||
#define IDC_PLEVEL_WIDTH 2572
|
||||
#define IDC_PLEVEL_QUALITY 2578
|
||||
#define IDC_PLEVEL_RESET 2584
|
||||
#define IDC_STATIC_SHARE_ADDRESS 2585
|
||||
#define IDC_EDIT_SHARE_ADDRESS 2586
|
||||
#define IDC_CHECK_USE_SHINE 2587
|
||||
#define IDC_STATIC_SHARE_DAYS 2588
|
||||
#define IDC_EDIT_SHARE_DAYS 2589
|
||||
#define IDC_FSPEED_SLIDER 2590
|
||||
#define IDC_FSPEED_LABEL 2591
|
||||
#define IDC_FSPEED_RESET 2592
|
||||
#define IDC_QLEVEL_LABEL_LEVEL 2593
|
||||
#define IDC_QLEVEL_LABEL_FPS 2594
|
||||
#define IDC_QLEVEL_LABEL_WIDTH 2595
|
||||
#define IDC_QLEVEL_LABEL_ALGO 2596
|
||||
#define IDC_QLEVEL_LABEL_BITRATE 2597
|
||||
#define IDC_QLEVEL_LABEL_LEVEL0 2598
|
||||
#define IDC_QLEVEL_LABEL_LEVEL1 2599
|
||||
#define IDC_QLEVEL_LABEL_LEVEL2 2600
|
||||
#define IDC_QLEVEL_LABEL_LEVEL3 2601
|
||||
#define IDC_QLEVEL_LABEL_LEVEL4 2602
|
||||
#define IDC_QLEVEL_LABEL_LEVEL5 2603
|
||||
#define IDC_QLEVEL_LABEL_HINT 2604
|
||||
#define IDC_PLEVEL_LABEL_LEVEL 2605
|
||||
#define IDC_PLEVEL_LABEL_WIDTH 2606
|
||||
#define IDC_PLEVEL_LABEL_QUALITY 2607
|
||||
#define IDC_PLEVEL_LABEL_LEVEL0 2608
|
||||
#define IDC_PLEVEL_LABEL_LEVEL1 2609
|
||||
#define IDC_PLEVEL_LABEL_LEVEL2 2610
|
||||
#define IDC_PLEVEL_LABEL_LEVEL3 2611
|
||||
#define IDC_PLEVEL_LABEL_LEVEL4 2612
|
||||
#define IDC_PLEVEL_LABEL_LEVEL5 2613
|
||||
#define IDC_PLEVEL_LABEL_HINT 2614
|
||||
#define IDC_FSPEED_HINT 2615
|
||||
#define IDC_FSPEED_LABEL_SLOWEST 2616
|
||||
#define IDC_FSPEED_LABEL_DEFAULT 2617
|
||||
#define IDC_FSPEED_LABEL_UNLIMITED 2618
|
||||
#define IDC_STATIC_TRIGGER_LIST 2619
|
||||
#define IDC_COMBO_INSTALL_LOC 2620
|
||||
#define IDD_QUALITY_LEVELS 3000
|
||||
#define IDD_PREVIEW_LEVELS 3001
|
||||
#define ID_ONLINE_UPDATE 32772
|
||||
#define ID_ONLINE_MESSAGE 32773
|
||||
#define ID_ONLINE_DELETE 32775
|
||||
@@ -1004,20 +1057,8 @@
|
||||
#define ID_MSGLOG_CLEAR 33041
|
||||
#define ID_CANCEL_SHARE 33042
|
||||
#define ID_MSGLOG_COPY 33043
|
||||
#define ID_MSGLOG_SEARCH 33078
|
||||
#define ID_MSGLOG_LOGIN_NOTIFY 33079
|
||||
#define ID_MSGLOG_BAN_IP 33080
|
||||
#define ID_MSGLOG_UNBAN_IP 33081
|
||||
#define ID_WEB_REMOTE_CONTROL 33044
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_MCP_SETTINGS 33077
|
||||
#define ID_CONNECTION_MANAGE 33086
|
||||
#define ID_CONNECTION_KILL 33087
|
||||
#define ID_CONNECTION_REFRESH 33088
|
||||
#define ID_TALK_MANAGE 33089
|
||||
#define ID_EXPERIMENT_FILE_MANAGER 33090
|
||||
#define ID_EXPERIMENT_PRIVATE_SCREEN 33091
|
||||
#define ID_EXPERIMENT_DRAWING_BOARD 33092
|
||||
#define ID_33046 33046
|
||||
#define ID_PROXY_PORT_AUTORUN 33047
|
||||
#define ID_TRIGGER_SETTINGS 33048
|
||||
@@ -1050,40 +1091,35 @@
|
||||
#define ID_ONLINE_CLIENT_LOG 33074
|
||||
#define ID_ONLINE_33075 33075
|
||||
#define ID_ONLINE_FORBIDDEN 33076
|
||||
#define ID_MCP_SETTINGS 33077
|
||||
#define ID_MSGLOG_SEARCH 33078
|
||||
#define ID_MSGLOG_LOGIN_NOTIFY 33079
|
||||
#define ID_MSGLOG_BAN_IP 33080
|
||||
#define ID_MSGLOG_UNBAN_IP 33081
|
||||
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
#define ID_TOOL_QUALITY_LEVELS 33083
|
||||
#define ID_TOOL_PREVIEW_LEVELS 33084
|
||||
#define IDC_QLEVEL_FPS 2547
|
||||
#define IDC_QLEVEL_WIDTH 2553
|
||||
#define IDC_QLEVEL_ALGO 2559
|
||||
#define IDC_QLEVEL_BITRATE 2565
|
||||
#define IDC_QLEVEL_RESET 2571
|
||||
#define IDC_PLEVEL_WIDTH 2572
|
||||
#define IDC_PLEVEL_QUALITY 2578
|
||||
#define IDC_PLEVEL_RESET 2584
|
||||
|
||||
#define IDD_DIALOG_SHARE_HOST 402
|
||||
#define IDC_STATIC_SHARE_ADDRESS 2585
|
||||
#define IDC_EDIT_SHARE_ADDRESS 2586
|
||||
#define IDC_CHECK_USE_SHINE 2587
|
||||
#define IDC_STATIC_SHARE_DAYS 2588
|
||||
#define IDC_EDIT_SHARE_DAYS 2589
|
||||
|
||||
// 文件传输速度调节对话框
|
||||
#define IDD_FILE_SPEED 403
|
||||
#define ID_TOOL_FILE_SPEED 33085
|
||||
#define IDC_FSPEED_SLIDER 2590
|
||||
#define IDC_FSPEED_LABEL 2591
|
||||
#define IDC_FSPEED_RESET 2592
|
||||
#define ID_CONNECTION_MANAGE 33086
|
||||
#define ID_CONNECTION_KILL 33087
|
||||
#define ID_CONNECTION_REFRESH 33088
|
||||
#define ID_TALK_MANAGE 33089
|
||||
#define ID_EXPERIMENT_FILE_MANAGER 33090
|
||||
#define ID_EXPERIMENT_PRIVATE_SCREEN 33091
|
||||
#define ID_EXPERIMENT_DRAWING_BOARD 33092
|
||||
#define ID_TLS_CERTIFICATE 33093
|
||||
#define ID_EXPERIMENT_TLS_ENABLE 33094
|
||||
#define ID_EXPERIMENT_TLS_DISABLE 33095
|
||||
#define ID_MSGLOG_WHITELIST_IP 33096
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 410
|
||||
#define _APS_NEXT_COMMAND_VALUE 33093
|
||||
#define _APS_NEXT_CONTROL_VALUE 2593
|
||||
#define _APS_NEXT_RESOURCE_VALUE 416
|
||||
#define _APS_NEXT_COMMAND_VALUE 33097
|
||||
#define _APS_NEXT_CONTROL_VALUE 2621
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
#endif
|
||||
@@ -386,6 +386,27 @@ void CMachineDlg::DeleteList()
|
||||
PostMessage(WM_SHOW_MSG, (WPARAM)new CString(_TR("请等待数据返回...")), 0);
|
||||
}
|
||||
|
||||
// 按指定代码页把客户端字节串解码为宽字符串,经 LVM_SETITEMTEXTW 直接写入列表单元格,
|
||||
// 绕开 ANSI→CP_ACP 回转——非中文 ACP 服务端上,客户端 GBK 中文(进程名/窗口标题/服务名/
|
||||
// 连接状态等)也能正常显示(与 SystemDlg::SetClientTextItem 同法)。
|
||||
static void SetMachineItemText(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
|
||||
{
|
||||
std::wstring w;
|
||||
if (sz && sz[0]) {
|
||||
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
w.resize(wlen - 1);
|
||||
MultiByteToWideChar(cp, 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);
|
||||
}
|
||||
|
||||
void CMachineDlg::ShowProcessList()
|
||||
{
|
||||
m_list.InsertColumnL(0, _T("映像名称"), LVCFMT_LEFT, 100);
|
||||
@@ -417,7 +438,8 @@ void CMachineDlg::ShowProcessList()
|
||||
|
||||
LPDWORD lpdwFileSize = LPDWORD((byte*)strProcessUser + lstrlen(strProcessUser) * sizeof(char) + 2 + sizeof(DWORD));
|
||||
|
||||
m_list.InsertItem(i, strExeFile, 0);
|
||||
m_list.InsertItem(i, "", 0);
|
||||
SetMachineItemText(m_list, i, 0, strExeFile, 936);
|
||||
|
||||
str.FormatL(_T("%5u"), *lpPID);
|
||||
m_list.SetItemText(i, 1, str);
|
||||
@@ -427,7 +449,7 @@ void CMachineDlg::ShowProcessList()
|
||||
str.FormatL(_T("%5u"), *lpdwThreads);
|
||||
m_list.SetItemText(i, 3, str);
|
||||
|
||||
m_list.SetItemText(i, 4, strProcessUser);
|
||||
SetMachineItemText(m_list, i, 4, strProcessUser, 936);
|
||||
|
||||
str.FormatL(_T("%5u K"), *lpdwWorkingSetSize);
|
||||
m_list.SetItemText(i, 5, str);
|
||||
@@ -435,9 +457,9 @@ void CMachineDlg::ShowProcessList()
|
||||
str.FormatL(_T("%5u KB"), *lpdwFileSize);
|
||||
m_list.SetItemText(i, 6, str);
|
||||
|
||||
m_list.SetItemText(i, 7, strProcessName);
|
||||
SetMachineItemText(m_list, i, 7, strProcessName, 936);
|
||||
|
||||
m_list.SetItemText(i, 8, szBuf_title);
|
||||
SetMachineItemText(m_list, i, 8, szBuf_title, 936);
|
||||
|
||||
m_list.SetItemText(i, 9, (*is64) ? _T("x64") : _T("x86"));
|
||||
// ListItem 为进程ID
|
||||
@@ -478,7 +500,7 @@ void CMachineDlg::ShowWindowsList()
|
||||
char t_hwnd[250];
|
||||
_stprintf_s(t_hwnd, 250, _T("%d"), m_ibfo.m_hwnd);
|
||||
m_list.SetItemText(i, 1, t_hwnd);
|
||||
m_list.SetItemText(i, 2, m_ibfo.strTitle);
|
||||
SetMachineItemText(m_list, i, 2, m_ibfo.strTitle, 936);
|
||||
m_list.SetItemText(i, 3, m_ibfo.canlook ? _L(_T("显示")) : _L(_T("隐藏")));
|
||||
str.FormatL(_T("%d*%d"), m_ibfo.w, m_ibfo.h);
|
||||
m_list.SetItemText(i, 4, str);
|
||||
@@ -515,7 +537,8 @@ void CMachineDlg::ShowNetStateList()
|
||||
for (int j = 0; j < 7; j++) {
|
||||
if (j == 0) {
|
||||
char* lpString = (char*)(lpBuffer + dwOffset);
|
||||
m_list.InsertItem(i, lpString, 0);
|
||||
m_list.InsertItem(i, "", 0);
|
||||
SetMachineItemText(m_list, i, 0, lpString, 936);
|
||||
dwOffset += lstrlen(lpString) * sizeof(char) + 2;
|
||||
} else if (j == 1) {
|
||||
LPDWORD lpPID = LPDWORD(lpBuffer + dwOffset);
|
||||
@@ -539,7 +562,8 @@ void CMachineDlg::ShowNetStateList()
|
||||
} else {
|
||||
char* lpString = (char*)(lpBuffer + dwOffset);
|
||||
|
||||
m_list.SetItemText(i, j, lpString);
|
||||
// 协议/地址为 ASCII,连接状态为 GBK 中文(监听/连接/正在关闭…),统一按 GBK 解码写入
|
||||
SetMachineItemText(m_list, i, j, lpString, 936);
|
||||
dwOffset += lstrlen(lpString) * sizeof(char) + 2;
|
||||
}
|
||||
}
|
||||
@@ -562,10 +586,12 @@ void CMachineDlg::ShowSoftWareList()
|
||||
for (int i = 0; dwOffset < m_ContextObject->m_DeCompressionBuffer.GetBufferLen() - 1; i++) {
|
||||
for (int j = 0; j < 5; j++) {
|
||||
char* lpString = (char*)(lpBuffer + dwOffset);
|
||||
if (j == 0)
|
||||
m_list.InsertItem(i, lpString, 0);
|
||||
else
|
||||
m_list.SetItemText(i, j, lpString);
|
||||
if (j == 0) {
|
||||
m_list.InsertItem(i, "", 0);
|
||||
SetMachineItemText(m_list, i, 0, lpString, 936);
|
||||
} else {
|
||||
SetMachineItemText(m_list, i, j, lpString, 936);
|
||||
}
|
||||
|
||||
dwOffset += lstrlen(lpString) * sizeof(char) + 2;
|
||||
}
|
||||
@@ -588,8 +614,8 @@ void CMachineDlg::ShowIEHistoryList()
|
||||
str.FormatL(_T("%d"), i);
|
||||
m_list.InsertItem(i, str, 0);
|
||||
m_list.SetItemText(i, 1, p_Browsinghistory->strTime);
|
||||
m_list.SetItemText(i, 2, p_Browsinghistory->strTitle);
|
||||
m_list.SetItemText(i, 3, p_Browsinghistory->strUrl);
|
||||
SetMachineItemText(m_list, i, 2, p_Browsinghistory->strTitle, 936);
|
||||
SetMachineItemText(m_list, i, 3, p_Browsinghistory->strUrl, 936);
|
||||
dwOffset += sizeof(Browsinghistory);
|
||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||
}
|
||||
@@ -606,10 +632,12 @@ void CMachineDlg::ShowFavoritesUrlList()
|
||||
for (int i = 0; dwOffset < m_ContextObject->m_DeCompressionBuffer.GetBufferLen() - 1; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
char* lpString = (char*)((char*)lpBuffer + dwOffset);
|
||||
if (j == 0)
|
||||
m_list.InsertItem(i, lpString, 0);
|
||||
else
|
||||
m_list.SetItemText(i, j, lpString);
|
||||
if (j == 0) {
|
||||
m_list.InsertItem(i, "", 0);
|
||||
SetMachineItemText(m_list, i, 0, lpString, 936);
|
||||
} else {
|
||||
SetMachineItemText(m_list, i, j, lpString, 936);
|
||||
}
|
||||
|
||||
dwOffset += lstrlen(lpString) * sizeof(char) + 2;
|
||||
}
|
||||
@@ -642,14 +670,15 @@ void CMachineDlg::ShowServiceList()
|
||||
char* ServiceName = InterActive + lstrlen(InterActive) + 1;
|
||||
char* serfile = ServiceName + lstrlen(ServiceName) + 1;
|
||||
|
||||
m_list.InsertItem(i, DisplayName, 0);
|
||||
m_list.SetItemText(i, 1, Describe);
|
||||
m_list.SetItemText(i, 2, serRunway);
|
||||
m_list.SetItemText(i, 3, serauto);
|
||||
m_list.SetItemText(i, 4, Login);
|
||||
m_list.SetItemText(i, 5, InterActive);
|
||||
m_list.SetItemText(i, 6, ServiceName);
|
||||
m_list.SetItemText(i, 7, serfile);
|
||||
m_list.InsertItem(i, "", 0);
|
||||
SetMachineItemText(m_list, i, 0, DisplayName, 936);
|
||||
SetMachineItemText(m_list, i, 1, Describe, 936);
|
||||
SetMachineItemText(m_list, i, 2, serRunway, 936);
|
||||
SetMachineItemText(m_list, i, 3, serauto, 936);
|
||||
SetMachineItemText(m_list, i, 4, Login, 936);
|
||||
SetMachineItemText(m_list, i, 5, InterActive, 936);
|
||||
SetMachineItemText(m_list, i, 6, ServiceName, 936);
|
||||
SetMachineItemText(m_list, i, 7, serfile, 936);
|
||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||
|
||||
dwOffset += lstrlen(DisplayName) + lstrlen(Describe) + lstrlen(serRunway) + lstrlen(serauto) +
|
||||
@@ -692,12 +721,24 @@ void CMachineDlg::ShowTaskList()
|
||||
continue;
|
||||
m_list.InsertItem(i, str);
|
||||
|
||||
// 目录列:只去掉末尾“\任务名”。不能用 Replace 全量替换——任务名也可能出现在父目录里,
|
||||
// 那样算出的目录是错的,执行/停止/删除任务会定位到错误的文件夹。
|
||||
str = taskpath;
|
||||
str.Replace(taskname, _T(""));
|
||||
m_list.SetItemText(i, 1, str);
|
||||
m_list.SetItemText(i, 2, taskname);
|
||||
m_list.SetItemText(i, 3, exepath);
|
||||
m_list.SetItemText(i, 4, status);
|
||||
{
|
||||
CString tail = _T("\\");
|
||||
tail += taskname;
|
||||
if (str.GetLength() >= tail.GetLength() &&
|
||||
str.Right(tail.GetLength()).CompareNoCase(tail) == 0) {
|
||||
str = str.Left(str.GetLength() - tail.GetLength());
|
||||
if (str.IsEmpty())
|
||||
str = _T("\\");
|
||||
}
|
||||
}
|
||||
// 任务名/目录/程序路径来自客户端 BSTRToUTF8,为 UTF-8;状态为 GBK 字面量(运行中/已禁用…)
|
||||
SetMachineItemText(m_list, i, 1, str.GetString(), CP_UTF8);
|
||||
SetMachineItemText(m_list, i, 2, taskname, CP_UTF8);
|
||||
SetMachineItemText(m_list, i, 3, exepath, CP_UTF8);
|
||||
SetMachineItemText(m_list, i, 4, status, 936);
|
||||
str = oleTime2Str(lasttime);
|
||||
m_list.SetItemText(i, 5, str);
|
||||
str = oleTime2Str(nexttime);
|
||||
@@ -722,10 +763,23 @@ void CMachineDlg::ShowHostsList()
|
||||
char* buf=nullptr;
|
||||
char* lpString = (char*)lpBuffer;
|
||||
const char* d = "\n";
|
||||
|
||||
// hosts 文件由客户端原样读盘后发送,未做编码转换(client/MachineManager.cpp SendHostsList)。
|
||||
// 记事本默认把 hosts 存成 UTF-8 带 BOM(EF BB BF);若按 GBK 解,首行会显示成 "锘?"。
|
||||
// 识别 BOM:UTF-8 按 UTF-8 解并跳过 BOM,其余仍按 GBK(无 BOM 的中文/英文 hosts)。
|
||||
// (UTF-16 hosts 极少见,此处未处理,保持原有行为。)
|
||||
UINT cp = 936;
|
||||
int dataLen = (int)m_ContextObject->m_DeCompressionBuffer.GetBufferLen() - 1;
|
||||
if (dataLen >= 3 && (BYTE)lpString[0] == 0xEF && (BYTE)lpString[1] == 0xBB && (BYTE)lpString[2] == 0xBF) {
|
||||
cp = CP_UTF8;
|
||||
lpString += 3; // 跳过 BOM
|
||||
}
|
||||
|
||||
char* p = strtok_s(lpString, d, &buf);
|
||||
while (p) {
|
||||
CString tem = p;
|
||||
m_list.InsertItem(i, tem);
|
||||
m_list.InsertItem(i, "");
|
||||
SetMachineItemText(m_list, i, 0, tem.GetString(), cp);
|
||||
p = strtok_s(NULL, d, &buf);
|
||||
m_list.SetItemData(i, (DWORD_PTR)new ListItem(m_list, i));
|
||||
i++;
|
||||
@@ -963,7 +1017,8 @@ void CMachineDlg::ShowProcessList_menu()
|
||||
p_InjectData->dwProcessID = dwProcessID;
|
||||
CString strexeis86 = m_list.GetItemText(nItem, 9);
|
||||
strexeis86 == _T("x86") ? p_InjectData->ExeIsx86 = 1 : p_InjectData->ExeIsx86 = 0;
|
||||
memcpy(p_InjectData->strpath, dlg.Str_remote, dlg.Str_remote.GetLength() * 2 + 2);
|
||||
// Str_remote 是窄字符串、strpath 是 char[1024]:按字符数+1 拷贝;超长时截断,避免堆溢出
|
||||
strncpy_s(p_InjectData->strpath, sizeof(p_InjectData->strpath), dlg.Str_remote, _TRUNCATE);
|
||||
//读取文件
|
||||
BYTE* lpBuffer = NULL;
|
||||
HANDLE hFile = CreateFile(dlg.Str_loacal, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
@@ -1369,16 +1424,18 @@ void CMachineDlg::ShowTaskList_menu()
|
||||
taskpath = m_list.GetItemText(nItem, 1);
|
||||
taskname = m_list.GetItemText(nItem, 2);
|
||||
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
|
||||
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
|
||||
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
|
||||
if (!lpBuffer) return;
|
||||
lpBuffer[0] = COMMAND_TASKSTART;
|
||||
offset++;
|
||||
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskpath.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskname.GetBuffer()) + 1;
|
||||
|
||||
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
|
||||
|
||||
@@ -1397,16 +1454,18 @@ void CMachineDlg::ShowTaskList_menu()
|
||||
taskpath = m_list.GetItemText(nItem, 1);
|
||||
taskname = m_list.GetItemText(nItem, 2);
|
||||
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
|
||||
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
|
||||
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
|
||||
if (!lpBuffer) return;
|
||||
lpBuffer[0] = COMMAND_TASKSTOP;
|
||||
offset++;
|
||||
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskpath.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskname.GetBuffer()) + 1;
|
||||
|
||||
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
|
||||
|
||||
@@ -1425,16 +1484,18 @@ void CMachineDlg::ShowTaskList_menu()
|
||||
taskpath = m_list.GetItemText(nItem, 1);
|
||||
taskname = m_list.GetItemText(nItem, 2);
|
||||
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) * 2 + lstrlen(taskname.GetBuffer()) * 2 + 5;
|
||||
// 服务端是 MultiByte 工程:按窄字符拼接,包长 = 1(令牌) + (目录+1) + (任务名+1)
|
||||
int nPacketLength = lstrlen(taskpath.GetBuffer()) + lstrlen(taskname.GetBuffer()) + 3;
|
||||
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, nPacketLength);
|
||||
if (!lpBuffer) return;
|
||||
lpBuffer[0] = COMMAND_TASKDEL;
|
||||
offset++;
|
||||
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskpath.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskpath.GetBuffer(), lstrlen(taskpath.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskpath.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(taskname.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, taskname.GetBuffer(), lstrlen(taskname.GetBuffer()) + 1);
|
||||
offset += lstrlen(taskname.GetBuffer()) + 1;
|
||||
|
||||
m_ContextObject->Send2Client(lpBuffer, nPacketLength);
|
||||
|
||||
@@ -1446,28 +1507,28 @@ void CMachineDlg::ShowTaskList_menu()
|
||||
DWORD offset = 0;
|
||||
CCreateTaskDlg* dlg = new CCreateTaskDlg(this);
|
||||
if (IDOK == dlg->DoModal()) {
|
||||
// 计算字符串长度
|
||||
len = lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + lstrlen(dlg->m_ExePath.GetBuffer()) *
|
||||
2 + lstrlen(dlg->m_Author.GetBuffer()) * 2 + lstrlen(dlg->m_Description.GetBuffer()) * 2 + 12;
|
||||
// 服务端是 MultiByte 工程:5 个窄字符串各带 1 字节结束符,再加 1 字节令牌
|
||||
len = lstrlen(dlg->m_TaskPath.GetBuffer()) + lstrlen(dlg->m_TaskNames.GetBuffer()) + lstrlen(dlg->m_ExePath.GetBuffer()) +
|
||||
lstrlen(dlg->m_Author.GetBuffer()) + lstrlen(dlg->m_Description.GetBuffer()) + 6;
|
||||
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, len);
|
||||
if (lpBuffer) {
|
||||
lpBuffer[0] = COMMAND_TASKCREAT;
|
||||
offset++;
|
||||
|
||||
memcpy(lpBuffer + offset, dlg->m_TaskPath.GetBuffer(), lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(dlg->m_TaskPath.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, dlg->m_TaskPath.GetBuffer(), lstrlen(dlg->m_TaskPath.GetBuffer()) + 1);
|
||||
offset += lstrlen(dlg->m_TaskPath.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, dlg->m_TaskNames.GetBuffer(), lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(dlg->m_TaskNames.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, dlg->m_TaskNames.GetBuffer(), lstrlen(dlg->m_TaskNames.GetBuffer()) + 1);
|
||||
offset += lstrlen(dlg->m_TaskNames.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, dlg->m_ExePath.GetBuffer(), lstrlen(dlg->m_ExePath.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(dlg->m_ExePath.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, dlg->m_ExePath.GetBuffer(), lstrlen(dlg->m_ExePath.GetBuffer()) + 1);
|
||||
offset += lstrlen(dlg->m_ExePath.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, dlg->m_Author.GetBuffer(), lstrlen(dlg->m_Author.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(dlg->m_Author.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, dlg->m_Author.GetBuffer(), lstrlen(dlg->m_Author.GetBuffer()) + 1);
|
||||
offset += lstrlen(dlg->m_Author.GetBuffer()) + 1;
|
||||
|
||||
memcpy(lpBuffer + offset, dlg->m_Description.GetBuffer(), lstrlen(dlg->m_Description.GetBuffer()) * 2 + 2);
|
||||
offset += lstrlen(dlg->m_Description.GetBuffer()) * 2 + 2;
|
||||
memcpy(lpBuffer + offset, dlg->m_Description.GetBuffer(), lstrlen(dlg->m_Description.GetBuffer()) + 1);
|
||||
offset += lstrlen(dlg->m_Description.GetBuffer()) + 1;
|
||||
m_ContextObject->Send2Client(lpBuffer, len);
|
||||
|
||||
LocalFree(lpBuffer);
|
||||
|
||||
Vendored
+6
-2
@@ -13,7 +13,9 @@
|
||||
"--http-port=8080"
|
||||
],
|
||||
"env": {
|
||||
"YAMA_WEB_ADMIN_PASS": "3.14159"
|
||||
"YAMA_WEB_ADMIN_PASS": "3.14159",
|
||||
"YAMA_TLS_CERT": "${workspaceFolder}/certs/server.crt",
|
||||
"YAMA_TLS_KEY": "${workspaceFolder}/certs/server.key"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"preLaunchTask": "sync-web-assets"
|
||||
@@ -30,7 +32,9 @@
|
||||
"--http-port=8080"
|
||||
],
|
||||
"env": {
|
||||
"YAMA_WEB_ADMIN_PASS": "3.14159"
|
||||
"YAMA_WEB_ADMIN_PASS": "3.14159",
|
||||
"YAMA_TLS_CERT": "${workspaceFolder}/certs/server.crt",
|
||||
"YAMA_TLS_KEY": "${workspaceFolder}/certs/server.key"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"buildFlags": "-gcflags='all=-N -l'",
|
||||
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user