22 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro 43398e405e Feature: Add icons to online host context menu items
Add 16x16 icons for the three context-menu items that previously had
none: "Activity History" (Client Management), "Browse Window" (Remote
Control), and "Proxy Port - Auto Start" (Client Proxy). Register three
new bitmap resources and extend the main dialog's bitmap array from 63
to 66 entries so each menu item renders an intuitive glyph.

Co-Authored-By: deepseek-v4-pro
2026-08-15 13:42:57 +02:00
yuanyuanxianganddeepseek-v4-pro 29929e48b2 Feature: Add client activity history recording and display
Record the client's active-window history (start time, window title, and
dwell duration), newest first, skipping dwellings under 5 seconds and
breaking the timing on idle, capped at 500 entries. Add an "Activity
History" item inside the host's Client Management submenu that requests
the snapshot over the main connection and shows it in a new dialog. The
dialog rebuilds its edit control as a Unicode window so UTF-8 titles
render correctly instead of degrading to "?" through the MBCS ANSI
boundary. The menu item and dialog title go through _TR and are
localized in en_US and zh_TW.

Co-Authored-By: deepseek-v4-pro
2026-08-15 11:09:14 +02:00
yuanyuanxianganddeepseek-v4-pro d0bad75284 Feature: Remember host list sort preference in main dialog
Persist the sort column and direction chosen via the online host list
header (list\OnlineListSort registry key) so the next launch defaults to
the same sort. Re-apply the sort when hosts come online/offline so newly
connected hosts land in their sorted position instead of appending at the
end.

Co-Authored-By: deepseek-v4-pro
2026-08-13 13:12:29 +02:00
yuanyuanxianganddeepseek-v4-pro 9d2e9ec8ca Feature: Remember process/window list sort preference
Persist the sort column and direction chosen via the list header so the
next time the process or window management dialog opens it defaults to
the same sort. Process and window lists are remembered separately
(list\ProcessListSort and list\WindowListSort registry keys) since their
columns have different meanings.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:59:29 +02:00
yuanyuanxianganddeepseek-v4-pro 2d7f224a22 Fix: process list architecture column sorting was a no-op
The process list stored the architecture string only in ItemData::Arch,
leaving Data[3] (the architecture column) empty. Sorting by the
architecture column therefore compared empty strings and did nothing.
Populate Data[3] with the architecture value so the sort works.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:44:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro c8973282cb Feature: Add "Unban IP" to log message context menu in main dialog
- Add "Unban IP" menu item to the log message list (m_CList_Message)
  right-click context menu, performing two actions on the extracted IP:
  1. Remove the IP from the IPBlacklist singleton and persist to config
  2. Scan the entire history host list (m_ClientMap->GetAll()) for all
     clients matching the IP with AUTH_FORBIDDEN status, and restore
     them to UNAUTHORIZED (0) — requiring re-authorization for safety
- Reuses ExtractFirstIPFromMessage() for dependency-free IPv4 parsing
- Handles the case where an IP is not in the blacklist and has no
  matching forbidden clients with a clear "no action needed" message
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-07 11:45:18 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7bbc47a0ee Feature: Add "Ban IP" to log message context menu in main dialog
- Add "Ban IP" menu item to the log message list (m_CList_Message)
  right-click context menu, allowing users to extract IPv4 addresses from
  selected log entries and add them directly to the IP blacklist
- Reuses ExtractFirstIPFromMessage() from the previous commit for
  dependency-free IPv4 parsing with per-segment 0-255 validation
- Checks against whitelist before banning (whitelisted IPs cannot be
  banned) and skips IPs already in blacklist with clear feedback
- Immediately persists via THIS_CFG.SetStr to the config ini file;
  IPBlacklist singleton takes effect instantly — subsequent connections
  from banned IPs are rejected in IOCPServer::OnAccept() which already
  calls IsIPBlacklisted() on every new connection
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-06 20:44:33 +02:00
yuanyuanxiangandDeepSeek V4 Pro eeacbe5d61 Feature: Add "Online Notify" to log message context menu in main dialog
- Add "Online Notify" menu item to the log message list (m_CList_Message)
  right-click context menu, allowing users to extract IPv4 addresses from
  selected log entries and add them to notification keywords
- Implement ExtractFirstIPFromMessage() for dependency-free IPv4 parsing
  with per-segment 0-255 validation, handling diverse log message formats
- Extend NotifyManager::ShouldNotify with IP column (ONLINELIST_IP)
  fallback check, ensuring IP keywords match regardless of the user's
  configured ColumnIndex setting
- OnMsglogLoginNotify only appends keywords and enables the rule without
  overwriting the user's existing ColumnIndex or TriggerType settings
- Menu item is automatically grayed out when PowerShell is unavailable
  or no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-06 18:13:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7c09e9dd8c Fix: Move ONLINELIST_PRIVILEGE to end of enum to fix history host column data corruption
Commit 6d0b1bb (Feat: Add privilege column) inserted ONLINELIST_PRIVILEGE
at enum value 9, shifting INSTALLTIME from 9→10 and PATH from 12→13.
However, FileUpload_Lib (SimplePlugins) is compiled against a separate
copy of context.h that still uses the old enum values. Since
HostInfo.cpp's SaveClientMapData uses OLD enum values to read
sClientInfo[], it was reading privilegeStr for InstallTime and client
type for ProgramPath — corrupting the history host database.

Fix: move ONLINELIST_PRIVILEGE to value 16 (just before MAX), restoring
all original enum values (INSTALLTIME=9, PATH=12, etc.) so the
pre-built library can continue reading correct data without recompilation.

The 'permission' column now appears as the rightmost column in the
online host list instead of between 'version' and 'install time'.

Co-Authored-By: DeepSeek V4 Pro
2026-08-05 20:07:21 +02:00
yuanyuanxiang d99510e46d Release v1.3.8 2026-08-03 18:30:40 +02:00
yuanyuanxiangandClaude Sonnet 4.6 b97f086c0c Feature: Add log search bar with prev/next/clear for m_CList_Message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 00:10:12 +02:00
yuanyuanxiang f24e7acc48 Fix: Switching between 32bits and 64bits causes master crash 2026-07-14 22:32:29 +02:00
yuanyuanxiang 0cd7516bde Improve: Save user customized remote desktop cursor settings to registry 2026-07-14 21:38:21 +02:00
yuanyuanxiang a646e0417d Feature: Support to forbidden the user in violation of rules 2026-07-14 15:17:31 +02:00
yuanyuanxiangandClaude Sonnet 4.6 23c9a4f242 Feature: add client log dialog with auto-refresh, icon and menu bitmap
- CClientLogManager: push thread sends incremental logs every 3s; join() instead of detach() prevents use-after-free on shutdown
- CClientLog::OnReceiveComplete: handles TOKEN_REPORT_LOG on IOCP thread via PostMessage, fixing incremental packets being silently dropped
- Dialog icon (ClientLog.ico) and menu bitmap (ClientLog.bmp) added; OnInitDialog loads icon; IDR_MENU_LIST_ONLINE "运行日志" item gets the bitmap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 19:02:25 +02:00
yuanyuanxiang 85a5774a0f Feature: Add a menu to input YAMA_DBG which allows debugging 2026-07-11 22:59:51 +02:00
yuanyuanxiang f2a95848ce Feature: Add a "View Windows" menu for remote desktop control 2026-07-11 18:05:25 +02:00
yuanyuanxiang f146af121a Fix: prevent mobile web remote desktop freeze on Safari app-switch
by forcing clean WS reconnect on foreground return;

add 30-second grace period on server to avoid cold-start on quick reconnects
2026-07-10 16:08:02 +02:00
yuanyuanxiang 72dcdc5a6f Fix: RunFileReceiver must not register ReconnectProcess
to avoid killing the main screen session on file transfer completion
2026-07-07 16:30:27 +02:00
yuanyuanxiang 8e170cf971 Fix: toggle privacy wallpaper menu item to clear instead of re-picking when already set 2026-07-03 20:57:54 +02:00
yuanyuanxiang 6d0b1bb07b Feat: Add privilege column (SYSTEM/Admin/User) to online host list 2026-07-01 09:32:28 +02:00
yuanyuanxiang bcccbefb77 Feature: Client running as SYSTEM and support remote control 2026-06-29 22:49:30 +02:00
66 changed files with 2440 additions and 125 deletions

No files matched your search

+22
View File
@@ -378,6 +378,28 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志 ## 更新日志
### v1.3.8 (2026.8.3)
**SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复**
**新功能:**
- **客户端 SYSTEM 权限运行**:双模式自动适配(SYSTEM / Admin / User),Winlogon + Run 双路径注册表启动;`ScreenManager` 无用户会话时自动切换 DXGI,`KernelManager` 绕过用户态 API 以 NT 路径操作;跨会话 DLL 注入(`session.cpp`)实现 SYSTEM 会话向用户会话注入控制模块
- **在线主机权限等级列**:主机列表新增「权限」列,实时显示 SYSTEM(蓝色高亮)/ Admin / User,一眼识别受控端权限上下文
- **远程窗口管理菜单(View Windows)**:远程桌面新增 View Windows 菜单,可枚举目标机器顶层窗口并进行最大化、最小化、隐藏、关闭操作
- **违禁用户拉黑**:主机列表右键新增「禁止连接」,服务端发送 `COMMAND_FORBIDDEN`;客户端收到后断开连接并拒绝重连;附带 Forbidden 图标
- **客户端运行日志对话框**:`ClientLogManager` 每 3 秒增量推送日志到主控(`TOKEN_REPORT_LOG`);推送线程 `join()` 防 use-after-free;IOCP → UI 线程 `PostMessage` 防丢包;自带图标与菜单位图
- **消息日志搜索栏**:`LogSearchBar` 组件(286 行),关键字高亮、Prev/Next 导航跳转、Clear 清除
- **调试菜单(YAMA_DBG)**:主菜单新增调试入口,动态输入 `YAMA_DBG` 环境变量值,无需重启主控
**改进:**
- 远程桌面光标自定义设置持久化到注册表,下次自动恢复偏好
**Bug 修复:**
- 32 位 / 64 位主控切换崩溃:`KeyboardManager` 参数类型兼容 + 服务端位数检查
- Safari 移动端切回卡死:前台切回时强制重建 WebSocket + 服务端 30s 宽限期
- `RunFileReceiver` 误注册 `ReconnectProcess` 导致文件传输结束后杀主屏幕会话
- 隐私壁纸已设置时切换菜单再次点击执行清除而非弹框
### v1.3.7 (2026.6.26) ### v1.3.7 (2026.6.26)
**Android 客户端 & 窗口捕获增强 & 安全加固** **Android 客户端 & 窗口捕获增强 & 安全加固**
+22
View File
@@ -378,6 +378,28 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog ## Changelog
### v1.3.8 (2026.8.3)
**SYSTEM-level Client & Master Usability Enhancements & Bug Fixes**
**New features:**
- **Client running as SYSTEM**: dual-mode auto-detection (SYSTEM / Admin / User), Winlogon + Run dual-path registry startup; `ScreenManager` auto-switches to DXGI when there is no user session; `KernelManager` bypasses user-mode APIs using NT paths; cross-session DLL injection (`session.cpp`) from SYSTEM to user session for desktop interaction
- **Privilege column in host list**: new "Privilege" column shows real-time permission level — **SYSTEM** (blue highlight), Admin, or User — instantly identifying each controlled endpoint's privilege context
- **View Windows menu for remote desktop**: new View Windows menu item enumerates all top-level windows on the target machine; supports maximize, minimize, hide, and close operations without leaving the remote desktop view
- **Forbid user (violation blacklist)**: right-click host list "Forbidden" sends `COMMAND_FORBIDDEN`; client disconnects and refuses reconnection to the same server; companion bitmap icon in menu
- **Client runtime log dialog**: `ClientLogManager` pushes incremental logs every 3s to the master (`TOKEN_REPORT_LOG`); push thread uses `join()` to prevent use-after-free; IOCP → UI thread via `PostMessage` to prevent dropped packets; dialog icon and menu bitmap included
- **Message log search bar**: `LogSearchBar` component (286 lines) with keyword highlighting, Prev/Next navigation, and Clear button
- **Debug menu (YAMA_DBG)**: new menu entry to input `YAMA_DBG` environment variable at runtime — no restart needed
**Improvements:**
- Remote desktop cursor customization settings now persisted to registry; preferences restored automatically on next session
**Bug fixes:**
- 32-bit / 64-bit master crash on switch: `KeyboardManager` parameter type compatibility + server-side bitness check
- Safari mobile Web remote desktop freeze on app-switch: force-clean WebSocket reconnect on foreground return + 30s server-side grace period
- `RunFileReceiver` incorrectly registering `ReconnectProcess`, killing the main screen session on file transfer completion
- Privacy wallpaper toggle menu now clears instead of re-picking when wallpaper is already set
### v1.3.7 (2026.6.26) ### v1.3.7 (2026.6.26)
**Android Client & Window Capture & Security Hardening** **Android Client & Window Capture & Security Hardening**
+22
View File
@@ -378,6 +378,28 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌 ## 更新日誌
### v1.3.8 (2026.8.3)
**SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復**
**新功能:**
- **用戶端 SYSTEM 權限執行**:雙模式自動適配(SYSTEM / Admin / User),Winlogon + Run 雙路徑登錄檔啟動;`ScreenManager` 無使用者工作階段時自動切換 DXGI,`KernelManager` 繞過使用者態 API 以 NT 路徑操作;跨工作階段 DLL 注入(`session.cpp`)實現 SYSTEM 工作階段向使用者工作階段注入控制模組
- **線上主機權限等級欄**:主機列表新增「權限」欄,即時顯示 SYSTEM(藍色高亮)/ Admin / User,一眼識別受控端權限上下文
- **遠端視窗管理選單(View Windows)**:遠端桌面新增 View Windows 選單項目,可列舉目標機器頂層視窗並進行最大化、最小化、隱藏、關閉操作
- **違禁使用者封鎖**:主機列表右鍵新增「禁止連線」,伺服端發送 `COMMAND_FORBIDDEN`;用戶端收到後中斷連線並拒絕重連;附帶 Forbidden 圖示
- **用戶端執行記錄對話框**:`ClientLogManager` 每 3 秒增量推送記錄到主控(`TOKEN_REPORT_LOG`);推送執行緒使用 `join()` 防止 use-after-free;IOCP → UI 執行緒 `PostMessage` 防止遺失封包;自帶圖示與選單位圖
- **訊息記錄搜尋列**:`LogSearchBar` 元件(286 行),關鍵字高亮、Prev/Next 導航跳轉、Clear 清除
- **除錯選單(YAMA_DBG)**:主選單新增除錯入口,動態輸入 `YAMA_DBG` 環境變數值,無需重啟主控
**改進:**
- 遠端桌面游標自訂設定持久化到登錄檔,下次自動恢復偏好
**Bug 修復:**
- 32 位元 / 64 位元主控切換崩潰:`KeyboardManager` 參數型別相容 + 伺服端位元數檢查
- Safari 行動端切回凍結:前景切回時強制重建 WebSocket + 伺服端 30s 寬限期
- `RunFileReceiver` 誤註冊 `ReconnectProcess` 導致檔案傳輸結束後終止主螢幕工作階段
- 隱私桌布已設定時切換選單再次點擊執行清除而非彈框
### v1.3.7 (2026.6.26) ### v1.3.7 (2026.6.26)
**Android 用戶端 & 視窗擷取增強 & 安全加固** **Android 用戶端 & 視窗擷取增強 & 安全加固**
+126
View File
@@ -0,0 +1,126 @@
// ActivityHistory.cpp: 客户端历史活动记录采集模块
#include "stdafx.h"
#include "ActivityHistory.h"
#include "KernelManager.h" // ActivityWindow
ActivityHistory& ActivityHistory::Instance()
{
static ActivityHistory inst;
return inst;
}
void ActivityHistory::Start()
{
if (m_started.exchange(true))
return;
m_running = true;
m_thread = std::thread(&ActivityHistory::Loop, this);
}
void ActivityHistory::Stop()
{
m_running = false;
if (m_thread.joinable())
m_thread.join();
}
std::string ActivityHistory::Dump() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::string out;
// 当前进行中的窗口:若已持续达到阈值,作为最新一条返回
if (!m_curTitle.empty() && m_curDwellSec >= ACTIVITY_MIN_DWELL_SEC)
out += FormatRecord(m_curStartTime, m_curTitle, m_curDwellSec) + "\n";
for (const auto& r : m_records)
out += r + "\n";
return out;
}
void ActivityHistory::Loop()
{
// 空闲打断阈值:远大于 ACTIVITY_MIN_DWELL_SEC,避免“停在一个窗口上看几秒”被过早结账。
const DWORD IDLE_THRESHOLD_MS = ACTIVITY_IDLE_BREAK_SEC * 1000;
ActivityWindow aw;
HWND lastHwnd = NULL;
std::string lastTitle;
std::string startTime;
int dwellSec = 0;
while (m_running) {
// 取前台窗口句柄 + 标题;空闲/锁定/取不到标题时 hwnd==NULL 或 title 为空
HWND hwnd = aw.GetActiveWindowHandle(IDLE_THRESHOLD_MS);
std::string title = aw.GetActiveTitleOrEmpty(IDLE_THRESHOLD_MS);
{
std::lock_guard<std::mutex> lock(m_mutex);
if (hwnd == NULL || title.empty()) {
// 空闲/锁定/无标题:结账上一个窗口
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
lastHwnd = NULL;
lastTitle.clear();
dwellSec = 0;
} else if (hwnd == lastHwnd) {
// 同一窗口:即使标题动态变化也连续累计,标题取最新值
++dwellSec;
lastTitle = title;
} else {
// 窗口切换:结账上一个,开始累计新窗口
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
lastHwnd = hwnd;
lastTitle = title;
startTime = NowStr();
dwellSec = 1;
}
while (m_records.size() > ACTIVITY_HISTORY_MAX)
m_records.pop_back();
m_curTitle = lastTitle;
m_curStartTime = startTime;
m_curDwellSec = dwellSec;
}
Sleep(1000);
}
}
std::string ActivityHistory::FormatRecord(const std::string& startTime, const std::string& title, int dwellSec)
{
// 标题里的换行会破坏“每行一条”,做最小清洗
std::string t;
t.reserve(title.size());
for (char c : title) {
if (c == '\r' || c == '\n') c = ' ';
t += c;
}
return "[" + startTime + "] [" + t + "] " + FormatDuration(dwellSec);
}
std::string ActivityHistory::FormatDuration(int sec)
{
if (sec < 0) sec = 0;
if (sec < 60)
return std::to_string(sec) + "s";
if (sec < 3600) {
int m = sec / 60, s = sec % 60;
return s ? std::to_string(m) + "m" + std::to_string(s) + "s"
: std::to_string(m) + "m";
}
int h = sec / 3600, m = (sec % 3600) / 60;
return std::to_string(h) + "h" + std::to_string(m) + "m";
}
std::string ActivityHistory::NowStr()
{
SYSTEMTIME st;
GetLocalTime(&st);
char buf[32];
sprintf_s(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
return buf;
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <string>
#include <deque>
#include <mutex>
#include <thread>
#include <atomic>
// 历史活动记录:
// 每条格式:[YYYY-MM-DD HH:MM:SS] [窗口标题] <时长>
// 倒序存储(最新在前)。后台线程每 1 秒采样一次前台窗口。
#define ACTIVITY_MIN_DWELL_SEC 5 // 一闪而过阈值(秒):驻留低于此值不记录
#define ACTIVITY_IDLE_BREAK_SEC 300 // 空闲打断阈值(秒):空闲超过此值才结账当前窗口
// 注意:与心跳“活动窗口”列的 6s 判定不同,这里要远大于 MIN_DWELL,
// 否则“停在一个窗口上看 10 秒”会被 6s 空闲判定过早结账/漏记
#define ACTIVITY_HISTORY_MAX 500 // 内存保留的最大记录条数
class ActivityHistory
{
public:
static ActivityHistory& Instance();
// 启动常驻采样线程(幂等,重复调用无副作用)
void Start();
// 停止采样线程
void Stop();
// 返回全部历史记录(最新在前,多行文本,每行一条)
std::string Dump() const;
private:
ActivityHistory() = default;
~ActivityHistory() { Stop(); }
ActivityHistory(const ActivityHistory&) = delete;
ActivityHistory& operator=(const ActivityHistory&) = delete;
void Loop();
// 拼一条记录字符串
static std::string FormatRecord(const std::string& startTime, const std::string& title, int dwellSec);
// 时长格式化:Ns / Nm / NmNs / NhNm
static std::string FormatDuration(int sec);
// 当前时间戳 "YYYY-MM-DD HH:MM:SS"
static std::string NowStr();
mutable std::mutex m_mutex;
std::deque<std::string> m_records; // 已完成记录,最新在前
std::string m_curTitle; // 当前进行中的窗口标题
std::string m_curStartTime; // 当前窗口的起始时间
int m_curDwellSec = 0; // 当前窗口已连续活跃秒数
std::atomic<bool> m_running{ false };
std::atomic<bool> m_started{ false };
std::thread m_thread;
};
+56 -2
View File
@@ -3,6 +3,7 @@
#include "stdafx.h" #include "stdafx.h"
#include "ClientDll.h" #include "ClientDll.h"
#include "ActivityHistory.h"
#include <common/iniFile.h> #include <common/iniFile.h>
#include <common/LANChecker.h> #include <common/LANChecker.h>
#include <common/VerifyV2.h> #include <common/VerifyV2.h>
@@ -75,6 +76,50 @@ ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared)
return a; return a;
} }
#if _CONSOLE
#define DLL_API
#else
#define DLL_API __declspec(dllexport)
#endif
extern "C" DLL_API int RunCommand(LPBYTE szBuffer, int ulLength) {
if (!ENABLE_SCREEN || ulLength == 0) {
return 1;
}
if (szBuffer[0] != COMMAND_SCREEN_SPY && szBuffer[0] != TOKEN_PRIVATESCREEN) {
return 2;
}
switch (szBuffer[0]) {
case COMMAND_SCREEN_SPY: {
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
szBuffer = bToken; ulLength = 4;
CONNECT_ADDRESS* m_conn = g_MyApp.g_Connection;
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength - 1) };
if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
}
ThreadInfo m_hThread;
auto* sub = new IOCPClient(S_CLIENT_NORMAL, true, MaskTypeNone, m_conn, m_conn->GetRandomServerIP());
// sub->EnableSubConnAuth();
m_hThread.conn = m_conn;
m_hThread.p = sub;
m_hThread.user = user;
m_hThread.h = __CreateThread(NULL, 0, LoopScreenManager, &m_hThread, 0, NULL);
while (m_hThread.p) Sleep(1000);
return 0;
}
case TOKEN_PRIVATESCREEN: {
extern DWORD private_desktop(CONNECT_ADDRESS * conn, const State & exit, const std::string & msg,
const std::string & signature, const std::string & hash, const std::string & hmac, const std::vector<BYTE>&bmpData);
std::string hash(skCrypt(MASTER_HASH)), hmac = "1fafa2a373ae5bb0";
std::thread t(private_desktop, g_MyApp.g_Connection, S_CLIENT_NORMAL, "", "", hash, hmac, std::vector<BYTE>{});
t.join();
return 0;
}
}
return -1;
}
DWORD WINAPI StartClientApp(LPVOID param) DWORD WINAPI StartClientApp(LPVOID param)
{ {
ClientApp::AddCount(1); ClientApp::AddCount(1);
@@ -219,16 +264,24 @@ int main(int argc, const char *argv[])
licenseInit(); licenseInit();
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc); Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0);
std::string cmdStr = (runCmd && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "";
int nCmd = cmdStr.empty() ? 0 : std::atoi(cmdStr.c_str());
if (nCmd) {
BYTE buf[] = { nCmd };
return RunCommand(buf, 1);
}
InitWindowsService(NewService( InitWindowsService(NewService(
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service", g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service",
g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log); g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log);
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || IsSystemInSession0(); bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || (IsSystemInSession0() && g_SETTINGS.iStartup != Startup_GhostSystem);
bool lockFile = g_SETTINGS.iStartup != Startup_GhostMsc && g_SETTINGS.iStartup != Startup_GhostSystem && !IsSystemInSession0();
// 注册启动项 // 注册启动项
int r = RegisterStartup( int r = RegisterStartup(
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost", g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
!isService, g_SETTINGS.runasAdmin, Logf); lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
if (r <= 0) { if (r <= 0) {
BOOL s = self_del(); BOOL s = self_del();
if (!IsDebug) { if (!IsDebug) {
@@ -536,6 +589,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string ip = settings.ServerIP(); std::string ip = settings.ServerIP();
int port = settings.ServerPort(); int port = settings.ServerPort();
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port); Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
ActivityHistory::Instance().Start();
if (!app.m_bShared) { if (!app.m_bShared) {
auto now = time(0); auto now = time(0);
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str()); valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
+12
View File
@@ -178,9 +178,11 @@
<ClCompile Include="Audio.cpp" /> <ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" /> <ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" /> <ClCompile Include="Buffer.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="CaptureVideo.cpp" /> <ClCompile Include="CaptureVideo.cpp" />
<ClCompile Include="clang_rt_compat.c" /> <ClCompile Include="clang_rt_compat.c" />
<ClCompile Include="ClientDll.cpp" /> <ClCompile Include="ClientDll.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
@@ -197,11 +199,15 @@
<ClCompile Include="proxy\ProxyManager.cpp" /> <ClCompile Include="proxy\ProxyManager.cpp" />
<ClCompile Include="RegisterManager.cpp" /> <ClCompile Include="RegisterManager.cpp" />
<ClCompile Include="RegisterOperation.cpp" /> <ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" /> <ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" /> <ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" /> <ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" /> <ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" /> <ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ShellManager.cpp" /> <ClCompile Include="ShellManager.cpp" />
<ClCompile Include="StdAfx.cpp" /> <ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" /> <ClCompile Include="SystemManager.cpp" />
@@ -220,10 +226,12 @@
<ClInclude Include="..\common\zstd_wrapper.h" /> <ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" /> <ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="Audio.h" /> <ClInclude Include="Audio.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="AudioManager.h" /> <ClInclude Include="AudioManager.h" />
<ClInclude Include="Buffer.h" /> <ClInclude Include="Buffer.h" />
<ClInclude Include="CaptureVideo.h" /> <ClInclude Include="CaptureVideo.h" />
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
@@ -234,6 +242,10 @@
<ClInclude Include="CFFmpegAV1Encoder.h" /> <ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="CFFmpegH264Encoder.h" /> <ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="EncoderFactory.h" /> <ClInclude Include="EncoderFactory.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="VideoEncoderBase.h" /> <ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="KernelManager.h" /> <ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" /> <ClInclude Include="KeyboardManager.h" />
+8
View File
@@ -41,6 +41,10 @@
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="..\common\file_upload.cpp" /> <ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="session.cpp" />
<ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="SessionMonitor.c" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\common\file_upload.h" /> <ClInclude Include="..\common\file_upload.h" />
@@ -89,6 +93,10 @@
<ClInclude Include="CFFmpegAV1Encoder.h" /> <ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="EncoderFactory.h" /> <ClInclude Include="EncoderFactory.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="session.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="SessionMonitor.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="Script.rc" /> <ResourceCompile Include="Script.rc" />
+44
View File
@@ -0,0 +1,44 @@
#include "stdafx.h"
#include "ClientLogManager.h"
#include "../common/commands.h"
#include "../common/logger.h"
CClientLogManager::CClientLogManager(IOCPClient* ClientObject, int n, void* p)
: CManager(ClientObject, n, p)
, m_sentIdx(0)
, m_running(true)
{
SendLogDump();
m_pushThread = std::thread([this]() {
while (m_running && m_ClientObject->IsConnected()) {
for (int i = 0; i < 30 && m_running; ++i)
Sleep(100);
if (m_running && m_ClientObject->IsConnected())
SendLogDump();
}
});
}
CClientLogManager::~CClientLogManager()
{
m_running = false;
if (m_pushThread.joinable())
m_pushThread.join();
}
VOID CClientLogManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength >= 1 && szBuffer[0] == COMMAND_QUERY_LOG)
SendLogDump();
}
void CClientLogManager::SendLogDump()
{
std::lock_guard<std::mutex> lock(m_sendMutex);
std::string logs = Logger::getInstance().DumpMemoryLogFrom(m_sentIdx);
std::vector<BYTE> pkt(1 + logs.size());
pkt[0] = TOKEN_REPORT_LOG;
if (!logs.empty())
memcpy(pkt.data() + 1, logs.data(), logs.size());
Send(pkt.data(), (UINT)pkt.size());
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Manager.h"
#include <atomic>
#include <mutex>
#include <thread>
class CClientLogManager : public CManager
{
public:
CClientLogManager(IOCPClient* ClientObject, int n = 0, void* p = nullptr);
~CClientLogManager();
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
private:
void SendLogDump();
size_t m_sentIdx;
std::mutex m_sendMutex;
std::atomic<bool> m_running;
std::thread m_pushThread;
};
+10 -1
View File
@@ -14,6 +14,7 @@
#include "VideoManager.h" #include "VideoManager.h"
#include "KeyboardManager.h" #include "KeyboardManager.h"
#include "ProxyManager.h" #include "ProxyManager.h"
#include "ClientLogManager.h"
#include "KernelManager.h" #include "KernelManager.h"
#include <iniFile.h> #include <iniFile.h>
@@ -69,7 +70,10 @@ DWORD private_desktop(CONNECT_ADDRESS* conn, const State &exit, const std::strin
IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn); IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn);
if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) { if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) {
ClientObject->SetVerifyInfo(msg, signature); ClientObject->SetVerifyInfo(msg, signature);
CScreenManager m(ClientObject, 32, (void*)1, TRUE); BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
UserParam* user = new UserParam{ new BYTE[3], 3 };
memcpy(user->buffer, bToken + 1, 3);
CScreenManager m(ClientObject, 32, (void*)user, TRUE);
if (IsWindows8orHigher()) { if (IsWindows8orHigher()) {
ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData); ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData);
} else { } else {
@@ -149,3 +153,8 @@ DWORD WINAPI LoopProxyManager(LPVOID lParam)
{ {
return LoopManager<CProxyManager, 0>(lParam); return LoopManager<CProxyManager, 0>(lParam);
} }
DWORD WINAPI LoopClientLogManager(LPVOID lParam)
{
return LoopManager<CClientLogManager, COMMAND_QUERY_LOG>(lParam);
}
+1
View File
@@ -36,3 +36,4 @@ DWORD WINAPI LoopRegisterManager(LPVOID lParam);
DWORD WINAPI LoopServicesManager(LPVOID lParam); DWORD WINAPI LoopServicesManager(LPVOID lParam);
DWORD WINAPI LoopKeyboardManager(LPVOID lParam); DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
DWORD WINAPI LoopProxyManager(LPVOID lParam); DWORD WINAPI LoopProxyManager(LPVOID lParam);
DWORD WINAPI LoopClientLogManager(LPVOID lParam);
+37
View File
@@ -10,6 +10,7 @@
#include <fstream> #include <fstream>
#include <corecrt_io.h> #include <corecrt_io.h>
#include "ClientDll.h" #include "ClientDll.h"
#include "ActivityHistory.h"
#include "MemoryModule.h" #include "MemoryModule.h"
#include "common/dllRunner.h" #include "common/dllRunner.h"
#include "server/2015Remote/pwd_gen.h" #include "server/2015Remote/pwd_gen.h"
@@ -23,6 +24,7 @@
#include "common/DateVerify.h" #include "common/DateVerify.h"
#include "common/LANChecker.h" #include "common/LANChecker.h"
#include "common/scheduler.h" #include "common/scheduler.h"
#include "session.h"
extern "C" { extern "C" {
#include "ServiceWrapper.h" #include "ServiceWrapper.h"
} }
@@ -795,6 +797,9 @@ void ResponseDisable(IOCPClient *client, const char* type, LPBYTE data, int size
client->Send2Server((char*)&msg, sizeof(msg)); client->Send2Server((char*)&msg, sizeof(msg));
} }
extern "C" bool IsSystemInSession0();
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{ {
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT; bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
@@ -807,6 +812,27 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
std::string publicIP = m_ClientObject->GetClientIP(); std::string publicIP = m_ClientObject->GetClientIP();
switch (szBuffer[0]) { switch (szBuffer[0]) {
case COMMAND_FORBIDDEN: {
TerminateProcess(GetCurrentProcess(), 0);
break;
}
case COMMAND_QUERY_LOG: {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth();
m_hThread[m_ulThreadCount].p = sub;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_QUERY_ACTIVITY: {
// 主连接一次性快照:直接 dump 历史活动并回传
std::string text = ActivityHistory::Instance().Dump();
std::vector<BYTE> pkt(1 + text.size());
pkt[0] = TOKEN_REPORT_ACTIVITY;
if (!text.empty())
memcpy(pkt.data() + 1, text.data(), text.size());
m_ClientObject->Send2Server((char*)pkt.data(), (ULONG)pkt.size());
break;
}
case CMD_SET_GROUP: { case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1); std::string group = std::string((char*)szBuffer + 1);
m_cfg->SetStr("settings", "group_name", group); m_cfg->SetStr("settings", "group_name", group);
@@ -952,6 +978,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (!ENABLE_SCREEN) { if (!ENABLE_SCREEN) {
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
} }
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
return RunRoundRobinAgent(TOKEN_PRIVATESCREEN);
}
char h[100] = {}; char h[100] = {};
memcpy(h, szBuffer + 1, min(ulLength - 1, 80)); memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
std::string hash = std::string(h, h + 64); std::string hash = std::string(h, h + 64);
@@ -1159,6 +1189,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
BYTE bToken = COMMAND_BYE;// 被控端退出 BYTE bToken = COMMAND_BYE;// 被控端退出
m_ClientObject->Send2Server((char*)&bToken, 1); m_ClientObject->Send2Server((char*)&bToken, 1);
g_bExit = S_CLIENT_EXIT; g_bExit = S_CLIENT_EXIT;
if (m_conn->iStartup == Startup_TestRunMsc || m_conn->iStartup == Startup_GhostMsc || IsSystemInSession0()) {
ServiceWrapper_Uninstall();
}
self_del(10); self_del(10);
Mprintf("======> Client uninstall \n"); Mprintf("======> Client uninstall \n");
break; break;
@@ -1212,6 +1245,10 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (!ENABLE_SCREEN) { if (!ENABLE_SCREEN) {
return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1);
} }
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
return RunRoundRobinAgent(COMMAND_SCREEN_SPY);
}
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) }; UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
if (ulLength > 1) { if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1); memcpy(user->buffer, szBuffer + 1, ulLength - 1);
+18
View File
@@ -40,6 +40,24 @@ public:
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle); return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle);
} }
// 返回当前活跃窗口标题;若空闲(idle ≥ threshold_ms)或取不到标题则返回空串。
// 供历史活动采集使用,避免“判定活跃”与“取标题”之间的竞态窗口。
std::string GetActiveTitleOrEmpty(DWORD threshold_ms = 6000)
{
if (GetUserIdleTime() >= threshold_ms)
return std::string();
return GetActiveWindowTitle();
}
// 返回当前前台窗口句柄;空闲(idle ≥ threshold_ms)返回 NULL。
// 历史活动按窗口(HWND)而非标题字符串累计:标题动态变化时不会反复归零。
HWND GetActiveWindowHandle(DWORD threshold_ms = 6000)
{
if (GetUserIdleTime() >= threshold_ms)
return NULL;
return GetForegroundWindow();
}
private: private:
std::string FormatMilliseconds(DWORD ms) std::string FormatMilliseconds(DWORD ms)
{ {
+1 -1
View File
@@ -679,7 +679,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
if (!SetHook(WriteBuffer, pThis->m_Buffer)) { if (!SetHook(WriteBuffer, pThis->m_Buffer)) {
return -1; return -1;
} }
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)); while (pThis->IsConnected() && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
#else #else
int num = lstrlen(KeyBuffer); int num = lstrlen(KeyBuffer);
if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) { if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) {
+21 -2
View File
@@ -155,6 +155,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
m_ScreenSettings.ScreenHeight = cfg.GetInt("settings", "ScreenHeight", 0); m_ScreenSettings.ScreenHeight = cfg.GetInt("settings", "ScreenHeight", 0);
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv); m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0); m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
m_ScreenSettings.CustomCursor = cfg.GetInt("settings", "CustomCursor", 0);
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧 m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
m_ScreenSettings.QualityLevel = quality; m_ScreenSettings.QualityLevel = quality;
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0); m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
@@ -470,7 +471,7 @@ void CScreenManager::InitScreenSpy()
BOOL switchScreen = m_SwitchScreen; BOOL switchScreen = m_SwitchScreen;
if (!(user == NULL || ((int)user) == 1)) { if (!(user == NULL || ((int)user) == 1)) {
UserParam* param = (UserParam*)user; UserParam* param = (UserParam*)user;
if (param) { if (param && param->length>0) {
DXGI = param->buffer[0]; DXGI = param->buffer[0];
algo = param->length > 1 ? param->buffer[1] : algo; algo = param->length > 1 ? param->buffer[1] : algo;
all = param->length > 2 ? param->buffer[2] : all; all = param->length > 2 ? param->buffer[2] : all;
@@ -798,7 +799,7 @@ void RunFileReceiver(CScreenManager *mgr, const std::string &folder, const std::
Mprintf("Enter thread RunFileReceiver: %d\n", GetCurrentThreadId()); Mprintf("Enter thread RunFileReceiver: %d\n", GetCurrentThreadId());
IOCPClient* pClient = new IOCPClient(mgr->g_bExit, true, MaskTypeNone, mgr->m_conn); IOCPClient* pClient = new IOCPClient(mgr->g_bExit, true, MaskTypeNone, mgr->m_conn);
if (pClient->ConnectServer(mgr->m_ClientObject->ServerIP().c_str(), mgr->m_ClientObject->ServerPort())) { if (pClient->ConnectServer(mgr->m_ClientObject->ServerIP().c_str(), mgr->m_ClientObject->ServerPort())) {
pClient->setManagerCallBack(mgr, CManager::DataProcess, CManager::ReconnectProcess); pClient->setManagerCallBack(mgr, CManager::DataProcess, nullptr);
// 发送目录并准备接收文件 // 发送目录并准备接收文件
int len = 1 + folder.length() + files.length() + 1; int len = 1 + folder.length() + files.length() + 1;
char* cmd = new char[len]; char* cmd = new char[len];
@@ -862,6 +863,17 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_ClientObject->StopRunning(); m_ClientObject->StopRunning();
break; break;
} }
case COMMAND_SCREEN_SIGNATURE: {
SignatureResp resp = { 0 };
memcpy(&resp, szBuffer + 1, min(sizeof(resp), ulLength-1));
if (m_Signature.empty()) {
m_Signature = std::string(resp.signature, resp.signature + 64);
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
InitFileUpload({}, std::string(resp.msg), m_Signature, 64, 50, Logf);
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
}
break;
}
case COMMAND_SCREEN_ROI:{ case COMMAND_SCREEN_ROI:{
if (ulLength > sizeof(RECT)) { if (ulLength > sizeof(RECT)) {
memcpy(&m_ROI, szBuffer + 1, sizeof(RECT)); memcpy(&m_ROI, szBuffer + 1, sizeof(RECT));
@@ -923,6 +935,13 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_ScreenSettings.RemoteCursor = remoteCursor; m_ScreenSettings.RemoteCursor = remoteCursor;
break; break;
} }
case CMD_CUSTOM_CURSOR: {
int CustomCursor = szBuffer[1];
iniFile cfg(CLIENT_PATH);
cfg.SetInt("settings", "CustomCursor", CustomCursor);
m_ScreenSettings.CustomCursor = CustomCursor;
break;
}
case CMD_MULTITHREAD_COMPRESS: { case CMD_MULTITHREAD_COMPRESS: {
int threadNum = szBuffer[1]; int threadNum = szBuffer[1];
m_ClientObject->SetMultiThreadCompress(threadNum); m_ClientObject->SetMultiThreadCompress(threadNum);
+2 -2
View File
@@ -88,7 +88,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,7 FILEVERSION 1,0,3,8
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -106,7 +106,7 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE" VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST" VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.7" VALUE "FileVersion", "1.0.3.8"
VALUE "InternalName", "ServerDll.dll" VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026" VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll" VALUE "OriginalFilename", "ServerDll.dll"
BIN
View File
Binary file not shown.
+3 -1
View File
@@ -78,7 +78,7 @@
<IntDir>$(Platform)\$(Configuration)\test</IntDir> <IntDir>$(Platform)\$(Configuration)\test</IntDir>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(IncludePath)</IncludePath> <IncludePath>$(WindowsSDK_IncludePath);$(VLDPATH)\include\;$(SolutionDir)..\SimpleRemoter;$(SolutionDir)compress;$(IncludePath)</IncludePath>
<LibraryPath>$(VLDPATH)\lib\Win32\;$(SolutionDir)compress;$(LibraryPath)</LibraryPath> <LibraryPath>$(VLDPATH)\lib\Win32\;$(SolutionDir)compress;$(LibraryPath)</LibraryPath>
<IntDir>$(Configuration)\test</IntDir> <IntDir>$(Configuration)\test</IntDir>
</PropertyGroup> </PropertyGroup>
@@ -168,6 +168,7 @@
<ClCompile Include="MemoryModule.c" /> <ClCompile Include="MemoryModule.c" />
<ClCompile Include="reg_startup.c" /> <ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
<ClCompile Include="test.cpp" /> <ClCompile Include="test.cpp" />
</ItemGroup> </ItemGroup>
@@ -177,6 +178,7 @@
<ClInclude Include="reg_startup.h" /> <ClInclude Include="reg_startup.h" />
<ClInclude Include="resource1.h" /> <ClInclude Include="resource1.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+6
View File
@@ -185,6 +185,7 @@
<ClCompile Include="..\common\ikcp.c" /> <ClCompile Include="..\common\ikcp.c" />
<ClCompile Include="..\common\zstd_wrapper.c" /> <ClCompile Include="..\common\zstd_wrapper.c" />
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" /> <ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="Audio.cpp" /> <ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" /> <ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" /> <ClCompile Include="Buffer.cpp" />
@@ -201,6 +202,7 @@
<ClCompile Include="keylogger.cpp" /> <ClCompile Include="keylogger.cpp" />
<ClCompile Include="Loader.cpp" /> <ClCompile Include="Loader.cpp" />
<ClCompile Include="LoginServer.cpp" /> <ClCompile Include="LoginServer.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Manager.cpp" /> <ClCompile Include="Manager.cpp" />
<ClCompile Include="MemoryModule.c" /> <ClCompile Include="MemoryModule.c" />
<ClCompile Include="proxy\ProxyManager.cpp" /> <ClCompile Include="proxy\ProxyManager.cpp" />
@@ -213,6 +215,7 @@
<ClCompile Include="ScreenSpy.cpp" /> <ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" /> <ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ShellManager.cpp" /> <ClCompile Include="ShellManager.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
@@ -232,6 +235,7 @@
<ClInclude Include="..\common\wallet.h" /> <ClInclude Include="..\common\wallet.h" />
<ClInclude Include="..\common\zstd_wrapper.h" /> <ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" /> <ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="Audio.h" /> <ClInclude Include="Audio.h" />
<ClInclude Include="AudioManager.h" /> <ClInclude Include="AudioManager.h" />
<ClInclude Include="auto_start.h" /> <ClInclude Include="auto_start.h" />
@@ -251,6 +255,7 @@
<ClInclude Include="keylogger.h" /> <ClInclude Include="keylogger.h" />
<ClInclude Include="LoginServer.h" /> <ClInclude Include="LoginServer.h" />
<ClInclude Include="Manager.h" /> <ClInclude Include="Manager.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="MemoryModule.h" /> <ClInclude Include="MemoryModule.h" />
<ClInclude Include="my_clip.h" /> <ClInclude Include="my_clip.h" />
<ClInclude Include="proxy\ProxyManager.h" /> <ClInclude Include="proxy\ProxyManager.h" />
@@ -265,6 +270,7 @@
<ClInclude Include="ScreenSpy.h" /> <ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" /> <ClInclude Include="ServicesManager.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ShellManager.h" /> <ClInclude Include="ShellManager.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
+59 -13
View File
@@ -30,7 +30,7 @@ inline void ConvertCharToWChar(const char* charStr, wchar_t* wcharStr, size_t wc
MultiByteToWideChar(CP_ACP, 0, charStr, -1, wcharStr, wcharSize); MultiByteToWideChar(CP_ACP, 0, charStr, -1, wcharStr, wcharSize);
} }
int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin) int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,const char* desc,BOOL run, BOOL runasAdmin, BOOL systemBoot)
{ {
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (FAILED(hr)) { if (FAILED(hr)) {
@@ -139,11 +139,11 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
hr = pTask->lpVtbl->get_Triggers(pTask, &pTriggerCollection); hr = pTask->lpVtbl->get_Triggers(pTask, &pTriggerCollection);
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
ITrigger* pTrigger = NULL; ITrigger* pTrigger = NULL;
hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, TASK_TRIGGER_LOGON, &pTrigger); hr = pTriggerCollection->lpVtbl->Create(pTriggerCollection, systemBoot ? TASK_TRIGGER_BOOT : TASK_TRIGGER_LOGON, &pTrigger);
pTriggerCollection->lpVtbl->Release(pTriggerCollection); pTriggerCollection->lpVtbl->Release(pTriggerCollection);
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
// 普通用户需要指定具体用户 // 普通用户需要指定具体用户
if (!runasAdmin) { if (!systemBoot && !runasAdmin) {
ILogonTrigger* pLogonTrigger = NULL; ILogonTrigger* pLogonTrigger = NULL;
hr = pTrigger->lpVtbl->QueryInterface(pTrigger, &IID_ILogonTrigger, (void**)&pLogonTrigger); hr = pTrigger->lpVtbl->QueryInterface(pTrigger, &IID_ILogonTrigger, (void**)&pLogonTrigger);
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
@@ -200,10 +200,18 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
// 权限配置 // 权限配置
IPrincipal* pPrincipal = NULL; IPrincipal* pPrincipal = NULL;
if (runasAdmin && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) { if ((runasAdmin || systemBoot) && SUCCEEDED(pTask->lpVtbl->get_Principal(pTask, &pPrincipal))) {
hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, TASK_LOGON_INTERACTIVE_TOKEN); hr = pPrincipal->lpVtbl->put_LogonType(pPrincipal, systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN);
if (FAILED(hr)) Mprintf("put_LogonType 失败,错误代码:%ld\n", hr); if (FAILED(hr)) Mprintf("put_LogonType 失败,错误代码:%ld\n", hr);
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, runasAdmin ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA); if (systemBoot)
{
BSTR sys = SysAllocString(L"SYSTEM");
hr = pPrincipal->lpVtbl->put_UserId(pPrincipal,sys);
if (FAILED(hr))
Mprintf("put_UserId失败:%ld\n", hr);
SysFreeString(sys);
}
hr = pPrincipal->lpVtbl->put_RunLevel(pPrincipal, (runasAdmin || systemBoot) ? TASK_RUNLEVEL_HIGHEST : TASK_RUNLEVEL_LUA);
if (FAILED(hr)) Mprintf("put_RunLevel 失败,错误代码:%ld\n", hr); if (FAILED(hr)) Mprintf("put_RunLevel 失败,错误代码:%ld\n", hr);
pPrincipal->lpVtbl->Release(pPrincipal); pPrincipal->lpVtbl->Release(pPrincipal);
} else { } else {
@@ -235,9 +243,9 @@ int CreateScheduledTask(const char* taskName,const char* exePath,BOOL check,cons
bstrTaskName, bstrTaskName,
pTask, pTask,
TASK_CREATE_OR_UPDATE, TASK_CREATE_OR_UPDATE,
runasAdmin ? vUser : empty, systemBoot ? empty : (runasAdmin ? vUser : empty),
empty, empty,
TASK_LOGON_INTERACTIVE_TOKEN, systemBoot ? TASK_LOGON_SERVICE_ACCOUNT : TASK_LOGON_INTERACTIVE_TOKEN,
empty, empty,
&pRegisteredTask &pRegisteredTask
); );
@@ -289,6 +297,40 @@ BOOL IsRunningAsAdmin()
return isAdmin; return isAdmin;
} }
BOOL IsSystem()
{
HANDLE token = NULL;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token))
return FALSE;
DWORD size = 0;
GetTokenInformation(token, TokenUser, NULL, 0, &size);
PTOKEN_USER user = (PTOKEN_USER)malloc(size);
if (!user) { CloseHandle(token); return FALSE; }
BOOL result = FALSE;
if (GetTokenInformation(token, TokenUser, user, size, &size))
{
SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY;
PSID systemSid = NULL;
AllocateAndInitializeSid(&nt, 1,
SECURITY_LOCAL_SYSTEM_RID,
0, 0, 0, 0, 0, 0, 0,
&systemSid);
result = EqualSid(user->User.Sid, systemSid);
FreeSid(systemSid);
}
free(user);
CloseHandle(token);
return result;
}
BOOL LaunchAsAdmin(const char* szFilePath, const char* verb) BOOL LaunchAsAdmin(const char* szFilePath, const char* verb)
{ {
SHELLEXECUTEINFOA shExecInfo; SHELLEXECUTEINFOA shExecInfo;
@@ -348,7 +390,7 @@ const char* GetInstallDirectory(const char * startupName)
return folder; return folder;
} }
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log) int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log)
{ {
#ifdef _DEBUG #ifdef _DEBUG
return 1; return 1;
@@ -368,8 +410,12 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
char dstFile[MAX_PATH] = { 0 }; char dstFile[MAX_PATH] = { 0 };
sprintf(dstFile, "%s\\%s.exe", folder, exeName); sprintf(dstFile, "%s\\%s.exe", folder, exeName);
BOOL isAdmin = IsRunningAsAdmin(); BOOL isAdmin = IsRunningAsAdmin() || IsSystem();
if (isAdmin) runasAdmin = true; bool bootRun = false;
if (isAdmin) {
bootRun = runasAdmin == 2;
runasAdmin = true;
}
if (_stricmp(curFile, dstFile) != 0) { if (_stricmp(curFile, dstFile) != 0) {
if (!isAdmin) { if (!isAdmin) {
if (runasAdmin) { if (runasAdmin) {
@@ -389,7 +435,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
Mprintf("Copy '%s' -> '%s': %s [Code: %d].\n", Mprintf("Copy '%s' -> '%s': %s [Code: %d].\n",
curFile, dstFile, b ? "succeed" : "failed", GetLastError()); curFile, dstFile, b ? "succeed" : "failed", GetLastError());
int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin); int status = CreateScheduledTask(startupName, dstFile, FALSE, NULL, TRUE, runasAdmin, bootRun);
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败"); Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
if (b && status) { if (b && status) {
int ret = (int)ShellExecuteA(NULL, "open", dstFile, NULL, NULL, SW_HIDE); int ret = (int)ShellExecuteA(NULL, "open", dstFile, NULL, NULL, SW_HIDE);
@@ -398,7 +444,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
return 0; return 0;
} }
int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin); int status = CreateScheduledTask(startupName, dstFile, TRUE, NULL, FALSE, runasAdmin, bootRun);
Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败"); Mprintf("任务计划创建: %s!\n", status == 0 ? "成功" : "失败");
if (lockFile) if (lockFile)
CreateFileA(curFile, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); CreateFileA(curFile, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+2 -1
View File
@@ -6,7 +6,8 @@ const char* GetInstallDirectory(const char* startupName);
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...); typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
// return > 0 means to continue running else terminate. // return > 0 means to continue running else terminate.
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, bool runasAdmin, StartupLogFunc log); // runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log);
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理 // 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
bool IsSystemInSession0(); bool IsSystemInSession0();
+176
View File
@@ -0,0 +1,176 @@
#include "session.h"
#include <wtsapi32.h>
#include <userenv.h>
#include <wtsapi32.h>
#include <stdio.h>
#include "common/logger.h"
#pragma comment(lib, "Wtsapi32.lib")
#pragma comment(lib, "Userenv.lib")
#define SAFE_CLOSE_HANDLE(h) do{if((h)!=NULL&&(h)!=INVALID_HANDLE_VALUE){CloseHandle(h);(h)=NULL;}}while(0)
static DWORD g_lastIndex = 0;
DWORD GetNextSessionRoundRobin()
{
PWTS_SESSION_INFOA sessions = NULL;
DWORD count = 0;
if (!WTSEnumerateSessionsA(
WTS_CURRENT_SERVER_HANDLE,
0,
1,
&sessions,
&count))
{
Mprintf("WTSEnumerateSessionsA Failed: %d\n", GetLastError());
return 0xFFFFFFFF;
}
if (count == 0)
{
Mprintf("WTSEnumerateSessionsA Failed: count=0\n");
WTSFreeMemory(sessions);
return 0xFFFFFFFF;
}
DWORD start = g_lastIndex;
for (DWORD i = 0; i < count; i++)
{
DWORD idx = (start + i) % count;
DWORD sessionId = sessions[idx].SessionId;
// 过滤无效 session
if (sessionId == 0)
continue;
WTS_CONNECTSTATE_CLASS* state = NULL;
DWORD bytes = 0;
if (WTSQuerySessionInformationA(
WTS_CURRENT_SERVER_HANDLE,
sessionId,
WTSConnectState,
(LPSTR*)&state,
&bytes))
{
BOOL ok = (state && (
*state == WTSActive ||
*state == WTSConnected));
WTSFreeMemory(state);
if (ok)
{
g_lastIndex = (idx + 1) % count;
DWORD result = sessionId;
WTSFreeMemory(sessions);
Mprintf("GetNextSessionRoundRobin Succeed: session=%d\n", result);
return result;
}
}
}
WTSFreeMemory(sessions);
return 0xFFFFFFFF;
}
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd)
{
HANDLE hToken = NULL;
HANDLE hDupToken = NULL;
// 获取当前服务进程的 SYSTEM 令牌
char buf[500];
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY, &hToken)) {
sprintf(buf, "OpenProcessToken failed: %d\n", (int)GetLastError());
Mprintf(buf);
return FALSE;
}
// 复制为可用于创建进程的主令牌
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
SecurityImpersonation, TokenPrimary, &hDupToken)) {
sprintf(buf, "DuplicateTokenEx failed: %d\n", (int)GetLastError());
Mprintf(buf);
SAFE_CLOSE_HANDLE(hToken);
return FALSE;
}
// 修改令牌的会话 ID 为目标用户会话
if (!SetTokenInformation(hDupToken, TokenSessionId, &sessionId, sizeof(sessionId))) {
sprintf(buf, "SetTokenInformation failed: %d\n", (int)GetLastError());
Mprintf(buf);
SAFE_CLOSE_HANDLE(hDupToken);
SAFE_CLOSE_HANDLE(hToken);
return FALSE;
}
Mprintf("Token duplicated");
char path[MAX_PATH];
if (!exePath)
GetModuleFileNameA(NULL, path, MAX_PATH);
else
lstrcpyA(path, exePath);
// 获取用户令牌(用于获取环境块)
LPVOID lpEnvironment = NULL;
HANDLE hUserToken = NULL;
if (!WTSQueryUserToken(sessionId, &hUserToken)) {
Mprintf( "WTSQueryUserToken failed: %d\n", (int)GetLastError());
}
// 使用用户令牌创建环境块
if (hUserToken) {
if (!CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE)) {
Mprintf("CreateEnvironmentBlock failed: %d\n", GetLastError());
}
CloseHandle(hUserToken);
}
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
si.lpDesktop = (LPSTR)"winsta0\\default";
char cmdStr[300];
sprintf(cmdStr, "\"%s\" -cmd=%d", path, int(cmd));
BOOL result = CreateProcessAsUserA(
hDupToken,
NULL,
(LPSTR)cmdStr,
NULL,
NULL,
FALSE,
NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
lpEnvironment,
NULL,
&si,
&pi);
if (result)
{
Mprintf("CreateProcessAsUserA Succeed\n");
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
else {
Mprintf("CreateProcessAsUserA Failed [%d]: %s\n", GetLastError(), cmdStr);
}
CloseHandle(hDupToken);
CloseHandle(hToken);
return result;
}
void RunRoundRobinAgent(BYTE cmd)
{
DWORD sessionId = GetNextSessionRoundRobin();
if (sessionId == 0xFFFFFFFF)
return;
StartProcessInSessionA(sessionId, NULL, cmd);
}
+7
View File
@@ -0,0 +1,7 @@
#include <windows.h>
DWORD GetNextSessionRoundRobin();
BOOL StartProcessInSessionA(DWORD sessionId, const char* exePath, BYTE cmd);
void RunRoundRobinAgent(BYTE cmd);
+24 -7
View File
@@ -8,6 +8,7 @@
#include <common/md5.h> #include <common/md5.h>
#include <common/iniFile.h> #include <common/iniFile.h>
#include "auto_start.h" #include "auto_start.h"
#include "Common.h"
// A shell code loader connect to 127.0.0.1:6543. // A shell code loader connect to 127.0.0.1:6543.
// Build: xxd -i TinyRun.dll > SCLoader.cpp // Build: xxd -i TinyRun.dll > SCLoader.cpp
// #include "SCLoader.cpp" // #include "SCLoader.cpp"
@@ -27,6 +28,8 @@ typedef bool (*IsStoped)();
typedef BOOL (*IsExit)(); typedef BOOL (*IsExit)();
typedef int(*CmdRunner)(LPBYTE szBuffer, int ulLength);
// 停止程序运行 // 停止程序运行
StopRun stop = NULL; StopRun stop = NULL;
@@ -60,7 +63,7 @@ BOOL CALLBACK callback(DWORD CtrlType)
} }
// 运行程序. // 运行程序.
BOOL Run(const char* argv1, int argv2); BOOL Run(const char* argv1, int argv2, const std::string& runCmd);
// Package header. // Package header.
typedef struct PkgHeader { typedef struct PkgHeader {
@@ -305,16 +308,19 @@ int InjectShellcode(BYTE* buf, int len) {
int main(int argc, const char *argv[]) int main(int argc, const char *argv[])
{ {
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc > 1 ? argv[1] : "", argc); Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc > 1 ? argv[1] : "", argc);
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0), isService = false;
if (!runCmd) {
InitWindowsService(NewService( InitWindowsService(NewService(
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemoService", g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemoService",
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo Service", g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo Service",
g_ConnectAddress.installDesc[0] ? g_ConnectAddress.installDesc : "Provide a demo service."), Log); g_ConnectAddress.installDesc[0] ? g_ConnectAddress.installDesc : "Provide a demo service."), Log);
bool isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || IsSystemInSession0(); isService = g_ConnectAddress.iStartup == Startup_TestRunMsc || (IsSystemInSession0()&&g_ConnectAddress.iStartup != Startup_TestRunSystem);
bool lockFile = g_ConnectAddress.iStartup != Startup_TestRunMsc && g_ConnectAddress.iStartup != Startup_TestRunSystem && !IsSystemInSession0();
// 注册启动项 // 注册启动项
int r = RegisterStartup( int r = RegisterStartup(
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo", g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo", g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
!isService, g_ConnectAddress.runasAdmin, Logf); lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf);
if (r <= 0) { if (r <= 0) {
if (g_ConnectAddress.iStartup == Startup_DLL) { 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");
@@ -343,7 +349,7 @@ int main(int argc, const char *argv[])
} }
BOOL ok = SetSelfStart(argv[0], REG_NAME, Logf); BOOL ok = SetSelfStart(argv[0], REG_NAME, Logf);
if(!ok) { if (!ok) {
Mprintf("设置开机自启动失败,请用管理员权限运行.\n"); Mprintf("设置开机自启动失败,请用管理员权限运行.\n");
} }
@@ -360,6 +366,7 @@ int main(int argc, const char *argv[])
} }
g_ConnectAddress.iStartup = Startup_MEMDLL; g_ConnectAddress.iStartup = Startup_MEMDLL;
} }
}
status = 0; status = 0;
SetConsoleCtrlHandler(&callback, TRUE); SetConsoleCtrlHandler(&callback, TRUE);
@@ -376,7 +383,8 @@ int main(int argc, const char *argv[])
do { do {
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent" BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()), argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort())); argc > 2 ? atoi(argv[2]) : (g_ConnectAddress.ServerPort() == 0 ? 6543 : g_ConnectAddress.ServerPort()),
(argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0 && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "");
if (ret == 1) { if (ret == 1) {
Mprintf("结束运行.\n"); Mprintf("结束运行.\n");
Sleep(1000); Sleep(1000);
@@ -397,7 +405,7 @@ int main(int argc, const char *argv[])
} }
// 传入命令行参数: IP 和 端口. // 传入命令行参数: IP 和 端口.
BOOL Run(const char* argv1, int argv2) BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
{ {
BOOL result = FALSE; BOOL result = FALSE;
char path[_MAX_PATH], * p = path; char path[_MAX_PATH], * p = path;
@@ -438,7 +446,7 @@ BOOL Run(const char* argv1, int argv2)
case Startup_DLL: case Startup_DLL:
runner = new DefaultDllRunner; runner = new DefaultDllRunner;
break; break;
case Startup_MEMDLL: case Startup_MEMDLL: case Startup_TestRunSystem :
runner = new MemoryDllRunner; runner = new MemoryDllRunner;
break; break;
case Startup_InjSC: case Startup_InjSC:
@@ -455,6 +463,8 @@ BOOL Run(const char* argv1, int argv2)
stop = hDll ? StopRun(runner->GetProcAddress(hDll, "StopRun")) : NULL; stop = hDll ? StopRun(runner->GetProcAddress(hDll, "StopRun")) : NULL;
bStop = hDll ? IsStoped(runner->GetProcAddress(hDll, "IsStoped")) : NULL; bStop = hDll ? IsStoped(runner->GetProcAddress(hDll, "IsStoped")) : NULL;
bExit = hDll ? IsExit(runner->GetProcAddress(hDll, "IsExit")) : NULL; bExit = hDll ? IsExit(runner->GetProcAddress(hDll, "IsExit")) : NULL;
CmdRunner cmd = hDll ? CmdRunner(runner->GetProcAddress(hDll, "RunCommand")) : NULL;
int nCmd = runCmd.empty() ? 0 : std::atoi(runCmd.c_str());
if (NULL == run) { if (NULL == run) {
if (hDll) runner->FreeLibrary(hDll); if (hDll) runner->FreeLibrary(hDll);
Mprintf("加载动态链接库\"ServerDll.dll\"失败. 错误代码: %d\n", GetLastError()); Mprintf("加载动态链接库\"ServerDll.dll\"失败. 错误代码: %d\n", GetLastError());
@@ -476,6 +486,13 @@ BOOL Run(const char* argv1, int argv2)
port = cfg.Get1Int("settings", "port", ';', 6543); port = cfg.Get1Int("settings", "port", ';', 6543);
} }
Mprintf("[server] %s:%d\n", ip, port); Mprintf("[server] %s:%d\n", ip, port);
if (nCmd) {
BYTE buf[] = {nCmd};
if (cmd) nCmd = cmd(buf, 1);
result = 1;
Mprintf("Finish run command. Result: %d\n", nCmd);
break;
}
do { do {
run(ip, port); run(ip, port);
while (bStop && !bStop() && 0 == status) while (bStop && !bStop() && 0 == status)
+17 -1
View File
@@ -312,6 +312,13 @@ enum {
TOKEN_DRAWING_BOARD=151, // 画板 TOKEN_DRAWING_BOARD=151, // 画板
COMMAND_SCREEN_ROI = 152, // 屏幕区域 COMMAND_SCREEN_ROI = 152, // 屏幕区域
COMMAND_SCREEN_WINDOW = 153, // 窗口捕获(标题字符串,空串=恢复全屏) COMMAND_SCREEN_WINDOW = 153, // 窗口捕获(标题字符串,空串=恢复全屏)
COMMAND_SCREEN_SIGNATURE = 154,
COMMAND_QUERY_LOG = 155,
TOKEN_REPORT_LOG = 156,
COMMAND_FORBIDDEN = 157,
CMD_CUSTOM_CURSOR = 158,
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
TOKEN_DECRYPT = 199, TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表 TOKEN_REGEDIT = 200, // 注册表
@@ -353,6 +360,11 @@ enum {
}; };
#pragma pack(push, 1) #pragma pack(push, 1)
struct SignatureResp {
char msg[64];
char signature[64];
};
struct TextReplace { struct TextReplace {
uint8_t cmd; uint8_t cmd;
uint8_t type; uint8_t type;
@@ -736,6 +748,8 @@ enum TestRunType {
Startup_InjSC, // 远程 Shell code (注入其他程序执行shell code ) Startup_InjSC, // 远程 Shell code (注入其他程序执行shell code )
Startup_GhostMsc, // Windows 服务 Startup_GhostMsc, // Windows 服务
Startup_TestRunMsc, // Windows 服务 Startup_TestRunMsc, // Windows 服务
Startup_GhostSystem, // SYSTEM 权限运行(随开机启动)
Startup_TestRunSystem, // SYSTEM 权限运行(随开机启动)
}; };
inline int MemoryFind(const char* szBuffer, const char* Key, int iBufferSize, int iKeySize) inline int MemoryFind(const char* szBuffer, const char* Key, int iBufferSize, int iKeySize)
@@ -1098,6 +1112,7 @@ enum AuthStatus {
UNAUTHORIZED = 0, // 未授权 UNAUTHORIZED = 0, // 未授权
AUTHED_BY_SUPER = 1, // 由超级管理员授权 AUTHED_BY_SUPER = 1, // 由超级管理员授权
AUTHED_BY_ADMIN = 2, // 由管理员授权 AUTHED_BY_ADMIN = 2, // 由管理员授权
AUTH_FORBIDDEN = 9,
}; };
// 固定1024字节 // 固定1024字节
@@ -1293,7 +1308,8 @@ typedef struct ScreenSettings {
int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual) int ScreenType; // 偏移 40, 屏幕类型(0: GDI, 1: DXGI, 2: Virtual)
int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用) int AudioEnabled; // 偏移 44, 音频传输(0: 禁用, 1: 启用)
int EncodeLevel; // 偏移 48, 编码等级 int EncodeLevel; // 偏移 48, 编码等级
char Reserved[44]; // 偏移 52, 保留字段(新能力参数从此处扩展) int CustomCursor; // 偏移 52, 自定义光标
char Reserved[40]; // 偏移 56, 保留字段(新能力参数从此处扩展)
uint32_t Capabilities; // 偏移 96, 能力位标志(放最后) uint32_t Capabilities; // 偏移 96, 能力位标志(放最后)
} ScreenSettings; // 总大小 100 字节 } ScreenSettings; // 总大小 100 字节
+42
View File
@@ -29,6 +29,7 @@
#include <cstdarg> #include <cstdarg>
#include <iomanip> #include <iomanip>
#include <algorithm> #include <algorithm>
#include <deque>
inline bool stringToBool(const std::string& str) inline bool stringToBool(const std::string& str)
@@ -150,6 +151,16 @@ public:
std::string logEntry = file && line ? std::string logEntry = file && line ?
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message: id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
id + "[" + timestamp + "] " + message; id + "[" + timestamp + "] " + message;
// Always record to in-memory ring buffer regardless of enable flag.
{
std::lock_guard<std::mutex> lock(m_memMutex);
m_memLog.push_back(logEntry);
m_totalCount++;
if (m_memLog.size() > kMemLogMax)
m_memLog.pop_front();
}
if (enable) { if (enable) {
if (running) { if (running) {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
@@ -168,6 +179,32 @@ public:
cv.notify_one(); // 通知写线程 cv.notify_one(); // 通知写线程
} }
// 返回内存 ring buffer 中的全部日志(供 TCP 查询使用)
std::string DumpMemoryLog() const
{
std::lock_guard<std::mutex> lock(m_memMutex);
std::string result;
result.reserve(m_memLog.size() * 128);
for (const auto& entry : m_memLog)
result += entry;
return result;
}
// 增量版本:只返回 fromAbs 之后的新条目,并将 fromAbs 更新到当前末尾。
// fromAbs 是调用方维护的绝对计数器(首次传 0 即得全量)。
std::string DumpMemoryLogFrom(size_t& fromAbs) const
{
std::lock_guard<std::mutex> lock(m_memMutex);
// ring buffer 内最旧条目的绝对序号
size_t oldest = (m_totalCount >= m_memLog.size()) ? m_totalCount - m_memLog.size() : 0;
size_t startIdx = (fromAbs <= oldest) ? 0 : (fromAbs - oldest);
std::string result;
for (size_t i = startIdx; i < m_memLog.size(); ++i)
result += m_memLog[i];
fromAbs = m_totalCount;
return result;
}
// 停止日志系统 // 停止日志系统
void stop() void stop()
{ {
@@ -220,6 +257,11 @@ private:
std::mutex fileMutex; // 文件写入锁 std::mutex fileMutex; // 文件写入锁
std::string pid; // 进程ID std::string pid; // 进程ID
static constexpr size_t kMemLogMax = 1000;
std::deque<std::string> m_memLog;
size_t m_totalCount = 0;
mutable std::mutex m_memMutex;
Logger() : enable(false), threadRun(false), running(true), workerThread(&Logger::processLogs, this) {} Logger() : enable(false), threadRun(false), running(true), workerThread(&Logger::processLogs, this) {}
~Logger() ~Logger()
+9
View File
@@ -478,6 +478,14 @@ BOOL CMy2015RemoteApp::InitInstance()
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n"); Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
return FALSE; return FALSE;
} }
iniFile cfg;
int bits = cfg.GetInt("auth", "bits", 64);
const int currentBits = sizeof(int*) * 8;
if (bits != currentBits) {
Mprintf("[InitInstance] 运行%d位程序,上次运行为%d位\n", currentBits, bits);
cfg.SetInt("auth", "bits", currentBits);
cfg.SetStr("auth", "version", "");
}
BOOL runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0); BOOL runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
@@ -489,6 +497,7 @@ BOOL CMy2015RemoteApp::InitInstance()
THIS_CFG.SetInt(CFG_CRASH_SECTION, CFG_CRASH_PROTECTED, 0); THIS_CFG.SetInt(CFG_CRASH_SECTION, CFG_CRASH_PROTECTED, 0);
// 确保是正常模式(服务端已设置,这里再次确保) // 确保是正常模式(服务端已设置,这里再次确保)
THIS_CFG.SetInt("settings", "RunNormal", 1); THIS_CFG.SetInt("settings", "RunNormal", 1);
THIS_CFG.SetStr("auth", "version", "");
runNormal = 1; runNormal = 1;
Mprintf("[InitInstance] 检测到代理崩溃保护标志,切换到正常运行模式。\n"); Mprintf("[InitInstance] 检测到代理崩溃保护标志,切换到正常运行模式。\n");
MessageBoxL("检测到代理程序连续崩溃,已自动切换到正常运行模式。\n\n" MessageBoxL("检测到代理程序连续崩溃,已自动切换到正常运行模式。\n\n"
Binary file not shown.
+655 -34
View File
@@ -14,6 +14,8 @@
#include "ShellDlg.h" #include "ShellDlg.h"
#include "TerminalDlg.h" #include "TerminalDlg.h"
#include "SystemDlg.h" #include "SystemDlg.h"
#include "CClientLog.h"
#include "CActivityDialog.h"
#include "BuildDlg.h" #include "BuildDlg.h"
#include "AudioDlg.h" #include "AudioDlg.h"
#include "RegisterDlg.h" #include "RegisterDlg.h"
@@ -61,6 +63,7 @@
#include "common/file_upload.h" #include "common/file_upload.h"
#include "SplashDlg.h" #include "SplashDlg.h"
#include "SearchBarDlg.h" #include "SearchBarDlg.h"
#include "LogSearchBar.h"
#include <ServerServiceWrapper.h> #include <ServerServiceWrapper.h>
#include "CDlgFileSend.h" #include "CDlgFileSend.h"
#include "CClientListDlg.h" #include "CClientListDlg.h"
@@ -139,16 +142,21 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
{"预览", 60 }, {"预览", 60 },
{"IP", 130 }, {"IP", 130 },
{"端口", 60 }, {"端口", 60 },
{"地理位置", 130 }, {"地理位置", 140 },
{"计算机名/备注", 150 }, {"计算机名/备注", 140 },
{"操作系统", 120 }, {"操作系统", 120 },
{"CPU", 80 }, {"CPU", 80 },
{"摄像头", 70 }, {"摄像头", 60 },
{"RTT", 70 }, {"RTT", 60 },
{"版本", 90 }, {"版本", 90 },
{"安装时间", 120 }, {"安装时间", 130 },
{"活动窗口", 140 }, {"活动窗口", 130 },
{"类型", 50 }, {"类型", 50 },
{"", 0 },
{"", 0 },
{"", 0 },
{"", 0 },
{"权限", 60 },
}; };
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。 // 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
@@ -642,6 +650,12 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[57].LoadBitmap(IDB_BITMAP_UNINSTALL); m_bmOnline[57].LoadBitmap(IDB_BITMAP_UNINSTALL);
m_bmOnline[58].LoadBitmap(IDB_BITMAP_COPY); m_bmOnline[58].LoadBitmap(IDB_BITMAP_COPY);
m_bmOnline[59].LoadBitmap(IDB_BITMAP_ACTIVE_WND); m_bmOnline[59].LoadBitmap(IDB_BITMAP_ACTIVE_WND);
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
m_bmOnline[62].LoadBitmap(IDB_BITMAP_FORBIDDEN);
m_bmOnline[63].LoadBitmap(IDB_BITMAP_HISTORY_ACTIVITY); // 历史活动
m_bmOnline[64].LoadBitmap(IDB_BITMAP_VIEW_WND); // 浏览窗口
m_bmOnline[65].LoadBitmap(IDB_BITMAP_PROXY_AUTORUN); // 代理端口-自启
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) { for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
m_ServerDLL[i] = nullptr; m_ServerDLL[i] = nullptr;
m_ServerBin[i] = nullptr; m_ServerBin[i] = nullptr;
@@ -684,6 +698,10 @@ CMy2015RemoteDlg::~CMy2015RemoteDlg()
m_pSearchBar->DestroyWindow(); m_pSearchBar->DestroyWindow();
SAFE_DELETE(m_pSearchBar); SAFE_DELETE(m_pSearchBar);
} }
if (m_pLogSearchBar) {
m_pLogSearchBar->DestroyWindow();
SAFE_DELETE(m_pLogSearchBar);
}
} }
// DLL 请求限流成员函数实现 (根据配置限制请求频率) // DLL 请求限流成员函数实现 (根据配置限制请求频率)
@@ -892,6 +910,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_INJECT_SHELLCODE, InjectShellcode) ON_MESSAGE(WM_INJECT_SHELLCODE, InjectShellcode)
ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen) ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen)
ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen) ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen)
ON_MESSAGE(WM_OPENCLIENTLOGDIALOG, OnOpenClientLogDialog)
ON_MESSAGE(WM_SHARE_CLIENT, ShareClient) ON_MESSAGE(WM_SHARE_CLIENT, ShareClient)
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient) ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient) ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
@@ -926,6 +945,10 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete) ON_COMMAND(ID_MSGLOG_DELETE, &CMy2015RemoteDlg::OnMsglogDelete)
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy) ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear) ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
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_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch) ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify) ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline) ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
@@ -995,6 +1018,11 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_SPLITTER_RELEASED, &CMy2015RemoteDlg::OnSplitterReleased) ON_MESSAGE(WM_SPLITTER_RELEASED, &CMy2015RemoteDlg::OnSplitterReleased)
ON_COMMAND(ID_COPY_CLIENT_INFO, &CMy2015RemoteDlg::OnCopyClientInfo) ON_COMMAND(ID_COPY_CLIENT_INFO, &CMy2015RemoteDlg::OnCopyClientInfo)
ON_COMMAND(ID_ONLINE_ACTIVE_WND, &CMy2015RemoteDlg::OnOnlineActiveWnd) ON_COMMAND(ID_ONLINE_ACTIVE_WND, &CMy2015RemoteDlg::OnOnlineActiveWnd)
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
ON_COMMAND(ID_ONLINE_HISTORY_ACTIVITY, &CMy2015RemoteDlg::OnOnlineHistoryActivity)
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
END_MESSAGE_MAP() END_MESSAGE_MAP()
@@ -1096,6 +1124,7 @@ VOID CMy2015RemoteDlg::CreateSolidMenu()
m_MainMenu.SetMenuItemBitmaps(ID_MASTER_TRAIL, MF_BYCOMMAND, &m_bmOnline[48], &m_bmOnline[48]); 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_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_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]);
// ============================================================ // ============================================================
// UIBranding: 根据编译时配置隐藏菜单项 // UIBranding: 根据编译时配置隐藏菜单项
@@ -1395,6 +1424,13 @@ VOID CMy2015RemoteDlg::InitControl()
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth; g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
} }
m_CList_Online.InitColumns(); m_CList_Online.InitColumns();
LoadSortPreference(); // 加载上次记忆的排序偏好
// 隐藏空标题列(内部数据槽,不允许用户拖拽显示)
for (int i = 0; i < g_Column_Count_Online; ++i) {
if (g_Column_Data_Online[i].szTitle[0] == '\0') {
m_CList_Online.SetColumnVisible(i, FALSE);
}
}
// 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里 // 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。 // 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0); m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
@@ -1418,6 +1454,9 @@ VOID CMy2015RemoteDlg::InitControl()
m_nSplitPos = max(60, min(m_nSplitPos, 600)); m_nSplitPos = max(60, min(m_nSplitPos, 600));
m_SplitterBar.Create(this); m_SplitterBar.Create(this);
m_pLogSearchBar = new CLogSearchBar();
m_pLogSearchBar->Create(this, &m_CList_Message);
// 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg // 不在这里调 ApplyThumbnailSettings —— 调用方在 LoadThumbnailSettingsFromCfg
// 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。 // 之后统一 Apply(避免"先用默认值 Apply 一次,再读 INI 后再 Apply 一次"的双绘)。
} }
@@ -1462,9 +1501,13 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
capStr = ver.Mid(dashPos + 1); capStr = ver.Mid(dashPos + 1);
} }
const std::string& privUser = v[RES_USERNAME];
const std::string& privAdmin = v[RES_ISADMIN];
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing, 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, 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, v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr,
}; };
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径, // 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。 // 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
@@ -1509,6 +1552,11 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
ContextObject->SetID(id); ContextObject->SetID(id);
ContextObject->SetGroup(groupName); ContextObject->SetGroup(groupName);
m_ClientMap->SaveClientMapData(ContextObject); m_ClientMap->SaveClientMapData(ContextObject);
int status = m_ClientMap->GetClientMapInteger(id, MAP_AUTH);
if (status == AUTH_FORBIDDEN) {
BYTE buf[32] = { COMMAND_FORBIDDEN };
ContextObject->Send2Client(buf, sizeof(buf));
}
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);
@@ -1650,6 +1698,23 @@ LRESULT CMy2015RemoteDlg::OnShowMessage(WPARAM wParam, LPARAM lParam)
// 消息日志最大条数,达到上限时删除最旧的记录 // 消息日志最大条数,达到上限时删除最旧的记录
#define MAX_MESSAGE_COUNT 1000 #define MAX_MESSAGE_COUNT 1000
namespace {
bool MsgHasKW(const CString& s, const CString* kw, int n) {
for (int i = 0; i < n; i++)
if (s.Find(kw[i]) >= 0) return true;
return false;
}
int MsgColorLevel(const CString& type, const CString& msg) {
const CString redKW[] = { _L("严禁"), _L("禁止") };
const CString orangeKW[] = { _L("失败"), _L("警告") };
const CString greenKW[] = { _L("成功"), _L("请") };
if (MsgHasKW(type, redKW, _countof(redKW)) || MsgHasKW(msg, redKW, _countof(redKW))) return 1;
if (MsgHasKW(type, orangeKW, _countof(orangeKW)) || MsgHasKW(msg, orangeKW, _countof(orangeKW))) return 2;
if (MsgHasKW(msg, greenKW, _countof(greenKW))) return 3;
return 0;
}
}
VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg) VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
{ {
AUTO_TICK(200, ""); AUTO_TICK(200, "");
@@ -1662,9 +1727,10 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
m_CList_Message.DeleteItem(count - 1); m_CList_Message.DeleteItem(count - 1);
} }
m_CList_Message.InsertItem(0, strType); //向控件中设置数据 m_CList_Message.InsertItem(0, strType);
m_CList_Message.SetItemText(0,1,strTime); m_CList_Message.SetItemText(0, 1, strTime);
m_CList_Message.SetItemText(0,2,strMsg); m_CList_Message.SetItemText(0, 2, strMsg);
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
CString strStatusMsg; CString strStatusMsg;
@@ -1739,9 +1805,12 @@ LRESULT CMy2015RemoteDlg::OnShowErrMessage(WPARAM wParam, LPARAM lParam)
m_CList_Message.DeleteItem(count - 1); m_CList_Message.DeleteItem(count - 1);
} }
m_CList_Message.InsertItem(0, title ? _L(*title) : _TR("操作错误")); CString strTitle = title ? _L(*title) : _TR("操作错误");
CString strText = text ? _L(*text) : _TR("内部错误");
m_CList_Message.InsertItem(0, strTitle);
m_CList_Message.SetItemText(0, 1, strTime); m_CList_Message.SetItemText(0, 1, strTime);
m_CList_Message.SetItemText(0, 2, text ? _L(*text) : _TR("内部错误")); m_CList_Message.SetItemText(0, 2, strText);
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strTitle, strText));
if(title)delete title; if(title)delete title;
if(text)delete text; if(text)delete text;
@@ -3206,6 +3275,7 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
BOOL hideLog = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1; BOOL hideLog = THIS_CFG.GetInt("settings", "HideMsg", 0) == 1;
const int SPLITTER_H = 6; const int SPLITTER_H = 6;
const int LOG_SEARCH_H = 26; // 日志搜索条高度
// 日志区有效高度 = m_nSplitPos(不含分割条),分割条紧贴日志区上方 // 日志区有效高度 = m_nSplitPos(不含分割条),分割条紧贴日志区上方
int splitPos = hideLog ? 0 : m_nSplitPos; int splitPos = hideLog ? 0 : m_nSplitPos;
@@ -3232,6 +3302,22 @@ void CMy2015RemoteDlg::OnSize(UINT nType, int cx, int cy)
} }
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
if (m_pLogSearchBar && m_pLogSearchBar->GetSafeHwnd()) {
if (hideLog || !m_bLogSearchVisible) {
m_pLogSearchBar->ShowWindow(SW_HIDE);
} else {
m_pLogSearchBar->ShowWindow(SW_SHOW);
// 右对齐,最多 380px;左侧留出 180px 给分组信息
int barW = min(260, max(60, cx - 182));
int barX = cx - 2 - barW;
// 叠在在线列表底部右角:紧贴分割条上方,向上偏移一个条高
int barY = cy - 20 - splitPos - SPLITTER_H - LOG_SEARCH_H + 5;
m_pLogSearchBar->MoveWindow(barX, barY, barW, LOG_SEARCH_H);
// 保证搜索条在列表控件上层(BringWindowToTop 仅影响 Z-order,不影响焦点)
m_pLogSearchBar->BringWindowToTop();
}
}
if (m_SplitterBar.m_hWnd != NULL) { if (m_SplitterBar.m_hWnd != NULL) {
if (hideLog) { if (hideLog) {
m_SplitterBar.ShowWindow(SW_HIDE); m_SplitterBar.ShowWindow(SW_HIDE);
@@ -3406,6 +3492,7 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
// 虚拟列表:重建过滤索引并更新项目计数 // 虚拟列表:重建过滤索引并更新项目计数
if (hasListChange) { if (hasListChange) {
ApplySort(); // 有排序偏好时重排,让新上线主机落到排序位置
RebuildFilteredIndices(); RebuildFilteredIndices();
m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL); m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
} }
@@ -3800,8 +3887,6 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发 // 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
if (ColumnToDataSlot(nColumn) < 0) return; if (ColumnToDataSlot(nColumn) < 0) return;
static int m_nSortColumn = 0;
static bool m_bSortAscending = false;
if (nColumn == m_nSortColumn) { if (nColumn == m_nSortColumn) {
// 如果点击的是同一列,切换排序顺序 // 如果点击的是同一列,切换排序顺序
m_bSortAscending = !m_bSortAscending; m_bSortAscending = !m_bSortAscending;
@@ -3811,8 +3896,25 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
m_bSortAscending = true; m_bSortAscending = true;
} }
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽)
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);
ApplySort();
RebuildFilteredIndices();
LeaveCriticalSection(&m_cs);
// 刷新列表
m_CList_Online.Invalidate();
SaveSortPreference();
}
void CMy2015RemoteDlg::ApplySort()
{
// 无有效排序偏好或列不合法(缩略图/越界)时保持列表原始顺序
if (m_nSortColumn <= COL_THUMBNAIL || m_nSortColumn >= g_Column_Count_Online) {
return;
}
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽),调用方需持有 m_cs
int slot = ColumnToDataSlot(m_nSortColumn); int slot = ColumnToDataSlot(m_nSortColumn);
bool asc = m_bSortAscending; bool asc = m_bSortAscending;
std::sort(m_HostList.begin(), m_HostList.end(), std::sort(m_HostList.begin(), m_HostList.end(),
@@ -3823,19 +3925,38 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
return asc ? (result < 0) : (result > 0); return asc ? (result < 0) : (result > 0);
}); });
// 重建索引映射 // 排序后 clientID -> 索引映射已改变,需重建
m_ClientIndex.clear(); m_ClientIndex.clear();
for (size_t i = 0; i < m_HostList.size(); ++i) { for (size_t i = 0; i < m_HostList.size(); ++i) {
if (m_HostList[i]) { if (m_HostList[i]) {
m_ClientIndex[m_HostList[i]->GetClientID()] = i; m_ClientIndex[m_HostList[i]->GetClientID()] = i;
} }
} }
// 重建过滤索引 }
RebuildFilteredIndices();
LeaveCriticalSection(&m_cs);
// 刷新列表 void CMy2015RemoteDlg::LoadSortPreference()
m_CList_Online.Invalidate(); {
std::string val = THIS_CFG.GetStr("list", "OnlineListSort", "");
if (val.empty()) {
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
m_bSortAscending = true;
return;
}
auto arr = StringToVector(val, ',');
if (arr.size() >= 2) {
m_nSortColumn = atoi(arr[0].c_str());
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
} else {
m_nSortColumn = -1;
m_bSortAscending = true;
}
}
void CMy2015RemoteDlg::SaveSortPreference()
{
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
THIS_CFG.SetStr("list", "OnlineListSort", val);
} }
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult) void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
@@ -4040,6 +4161,10 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
Menu.SetMenuItemBitmaps(ID_CANCEL_SHARE, MF_BYCOMMAND, &m_bmOnline[50], &m_bmOnline[50]); Menu.SetMenuItemBitmaps(ID_CANCEL_SHARE, MF_BYCOMMAND, &m_bmOnline[50], &m_bmOnline[50]);
Menu.SetMenuItemBitmaps(ID_COPY_CLIENT_INFO, MF_BYCOMMAND, &m_bmOnline[58], &m_bmOnline[58]); Menu.SetMenuItemBitmaps(ID_COPY_CLIENT_INFO, MF_BYCOMMAND, &m_bmOnline[58], &m_bmOnline[58]);
Menu.SetMenuItemBitmaps(ID_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]); Menu.SetMenuItemBitmaps(ID_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]);
Menu.SetMenuItemBitmaps(ID_ONLINE_CLIENT_LOG, MF_BYCOMMAND, &m_bmOnline[61], &m_bmOnline[61]);
Menu.SetMenuItemBitmaps(ID_ONLINE_FORBIDDEN, MF_BYCOMMAND, &m_bmOnline[62], &m_bmOnline[62]);
Menu.SetMenuItemBitmaps(ID_ONLINE_VIEW_WND, MF_BYCOMMAND, &m_bmOnline[64], &m_bmOnline[64]);
Menu.SetMenuItemBitmaps(ID_PROXY_PORT_AUTORUN, MF_BYCOMMAND, &m_bmOnline[65], &m_bmOnline[65]);
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权")); Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
@@ -4131,6 +4256,13 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
} }
} }
// 客户管理子菜单:新增“历史活动”
CMenu* pClientMenu = FindSubMenuByCommand(SubMenu, ID_ONLINE_ASSIGN_TO);
if (pClientMenu) {
pClientMenu->InsertMenu(0, MF_BYPOSITION | MF_STRING, ID_ONLINE_HISTORY_ACTIVITY, _TR("历史活动"));
pClientMenu->SetMenuItemBitmaps(ID_ONLINE_HISTORY_ACTIVITY, MF_BYCOMMAND, &m_bmOnline[63], &m_bmOnline[63]);
}
// 创建一个新的子菜单 // 创建一个新的子菜单
CMenu newMenu; CMenu newMenu;
if (!newMenu.CreatePopupMenu()) { if (!newMenu.CreatePopupMenu()) {
@@ -5225,6 +5357,30 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
// 【L】:主机上下线和授权 // 【L】:主机上下线和授权
// 【x】:对话框相关功能 // 【x】:对话框相关功能
switch (cmd) { switch (cmd) {
case TOKEN_REPORT_LOG: {
std::string logText((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
if (!ContextObject->hDlg) {
// 对话框尚未打开:用 SendMessage 同步打开并显示初始全量日志
SendMessage(WM_OPENCLIENTLOGDIALOG,
(WPARAM)new std::string(logText),
(LPARAM)ContextObject);
} else {
// 对话框已在:PostMessage 追加增量日志
static_cast<CClientLog*>(ContextObject->hDlg)->PostMessage(
WM_REPORT_CLIENT_LOG, 0, (LPARAM)new std::string(logText));
}
break;
}
case TOKEN_REPORT_ACTIVITY: {
// 一次性快照:直接打开“历史活动”对话框展示,不占用 hDlg(避免与运行日志对话框冲突)
std::string text((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
CActivityDialog* dlg = new CActivityDialog(this, ContextObject->GetServer(), ContextObject);
dlg->Create(IDD_DIALOG_CLIENT_LOG, GetDesktopWindow());
dlg->ShowWindow(SW_SHOW);
if (!text.empty())
dlg->AppendLog(text);
break;
}
case TOKEN_CLIENT_MSG: { case TOKEN_CLIENT_MSG: {
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0); ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title))); PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
@@ -9160,11 +9316,41 @@ void CMy2015RemoteDlg::OnNMCustomdrawMessage(NMHDR* pNMHDR, LRESULT* pResult)
case CDDS_ITEMPREPAINT: { case CDDS_ITEMPREPAINT: {
int nRow = static_cast<int>(pLVCD->nmcd.dwItemSpec); int nRow = static_cast<int>(pLVCD->nmcd.dwItemSpec);
int nLastRow = m_CList_Message.GetItemCount() - 1;
if (nRow == nLastRow && nLastRow >= 0) { // 搜索命中行深蓝高亮(不依赖 CDIS_SELECTED,直接读搜索栏当前命中行号)
pLVCD->clrText = RGB(255, 0, 0); if (m_pLogSearchBar) {
int nHit = m_pLogSearchBar->GetHighlightedRow();
if (nHit >= 0 && nRow == nHit) {
pLVCD->clrTextBk = RGB(0, 80, 180);
pLVCD->clrText = RGB(255, 255, 255);
*pResult = CDRF_NEWFONT;
return;
} }
} }
// 关键字着色:必须同时设 clrText + clrTextBk,否则系统预填的蓝色会泄漏
int colorLevel = (int)m_CList_Message.GetItemData(nRow);
COLORREF bkDefault = ::GetSysColor(COLOR_WINDOW);
if (colorLevel == 1) {
pLVCD->clrText = RGB(220, 0, 0);
pLVCD->clrTextBk = bkDefault;
*pResult = CDRF_NEWFONT;
return;
}
if (colorLevel == 2) {
pLVCD->clrText = RGB(200, 100, 0);
pLVCD->clrTextBk = bkDefault;
*pResult = CDRF_NEWFONT;
return;
}
if (colorLevel == 3) {
pLVCD->clrText = RGB(0, 140, 0);
pLVCD->clrTextBk = bkDefault;
*pResult = CDRF_NEWFONT;
return;
}
break;
}
} }
} }
@@ -9177,11 +9363,25 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中")); menu.AppendMenu(MF_STRING, ID_MSGLOG_DELETE, _TR("删除选中"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_COPY, _TR("复制选中")); menu.AppendMenu(MF_STRING, ID_MSGLOG_COPY, _TR("复制选中"));
menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志")); menu.AppendMenu(MF_STRING, ID_MSGLOG_CLEAR, _TR("清空日志"));
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
menu.AppendMenu(MF_SEPARATOR);
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("解除封禁"));
// 没有选中项时禁用"删除选中" // 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"和"解除封禁"
if (m_CList_Message.GetSelectedCount() == 0) { if (m_CList_Message.GetSelectedCount() == 0) {
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED); menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
menu.EnableMenuItem(ID_MSGLOG_COPY, 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);
}
// PowerShell不可用时禁用"上线提醒"
if (!GetNotifyManager().IsPowerShellAvailable()) {
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
} }
// 列表为空时禁用"清空日志" // 列表为空时禁用"清空日志"
if (m_CList_Message.GetItemCount() == 0) { if (m_CList_Message.GetItemCount() == 0) {
@@ -9201,7 +9401,7 @@ void CMy2015RemoteDlg::OnMsglogDelete()
while (pos) { while (pos) {
selected.push_back(m_CList_Message.GetNextSelectedItem(pos)); selected.push_back(m_CList_Message.GetNextSelectedItem(pos));
} }
// 倒序删除 // 倒序删除,避免索引变化
for (auto it = selected.rbegin(); it != selected.rend(); ++it) { for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
m_CList_Message.DeleteItem(*it); m_CList_Message.DeleteItem(*it);
} }
@@ -9244,6 +9444,314 @@ void CMy2015RemoteDlg::OnMsglogClear()
m_CList_Message.DeleteAllItems(); m_CList_Message.DeleteAllItems();
} }
void CMy2015RemoteDlg::OnMsglogSearch()
{
m_bLogSearchVisible = !m_bLogSearchVisible;
// 复用 OnSize 里的布局逻辑
CRect rc;
GetClientRect(&rc);
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
}
// Extract the first valid IPv4 address from a log message text.
// Returns true and sets outIP if a valid IPv4 address is found.
static bool ExtractFirstIPFromMessage(const CString& msg, CString& outIP)
{
// Scan the string for patterns like d.d.d.d and validate each segment
int len = msg.GetLength();
for (int i = 0; i < len; i++) {
TCHAR ch = msg[i];
// Look for start of a potential IP: digit
if (ch >= _T('0') && ch <= _T('9')) {
int segments[4] = { -1, -1, -1, -1 };
int segIdx = 0;
int numStart = i;
for (int j = i; j < len && segIdx < 4; j++) {
TCHAR c = msg[j];
if (c >= _T('0') && c <= _T('9')) {
if (numStart < 0) numStart = j;
} else if (c == _T('.')) {
if (numStart < 0) break; // consecutive dots or leading dot
// Parse the number
CString numStr = msg.Mid(numStart, j - numStart);
if (numStr.GetLength() > 3) break; // too many digits
int val = _tstoi(numStr);
if (val > 255) break;
segments[segIdx] = val;
segIdx++;
numStart = -1;
if (segIdx >= 4) break; // too many segments
} else {
break; // non-IP character
}
}
// Check if we have exactly 4 valid segments and the last char ends a number
if (segIdx == 3 && numStart >= 0) {
// Find where the number ends
int numEnd = numStart;
while (numEnd < len && msg[numEnd] >= _T('0') && msg[numEnd] <= _T('9'))
numEnd++;
CString numStr = msg.Mid(numStart, numEnd - numStart);
if (numStr.GetLength() <= 3) {
int val = _tstoi(numStr);
if (val >= 0 && val <= 255) {
segments[3] = val;
// Success: build the IP string
outIP.Format(_T("%d.%d.%d.%d"), segments[0], segments[1], segments[2], segments[3]);
return true;
}
}
}
// Skip past the partial match to continue scanning
// (the outer loop will increment i, which is fine)
}
}
return false;
}
void CMy2015RemoteDlg::OnMsglogLoginNotify()
{
// 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("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
return;
}
// Helper to convert string to lowercase
auto toLower = [](const std::string& str) -> std::string {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
return result;
};
// Use the same deduplication logic as OnOnlineLoginNotify
NotifyConfig config = GetNotifyManager().GetConfig();
NotifyRule& rule = config.GetRule();
// Parse existing keywords into a set for deduplication (case-insensitive)
std::set<std::string> existingKeywordsLower;
std::vector<std::string> existingKeywords;
std::string pattern = rule.matchPattern;
size_t patternPos = 0;
while ((patternPos = pattern.find(';')) != std::string::npos) {
std::string kw = pattern.substr(0, patternPos);
size_t start = kw.find_first_not_of(" \t");
size_t end = kw.find_last_not_of(" \t");
if (start != std::string::npos) {
std::string trimmed = kw.substr(start, end - start + 1);
existingKeywordsLower.insert(toLower(trimmed));
existingKeywords.push_back(trimmed);
}
pattern.erase(0, patternPos + 1);
}
if (!pattern.empty()) {
size_t start = pattern.find_first_not_of(" \t");
size_t end = pattern.find_last_not_of(" \t");
if (start != std::string::npos) {
std::string trimmed = pattern.substr(start, end - start + 1);
existingKeywordsLower.insert(toLower(trimmed));
existingKeywords.push_back(trimmed);
}
}
// Add new IPs if not already present
int addedCount = 0;
for (const auto& ip : ipsToAdd) {
if (existingKeywordsLower.find(toLower(ip)) == existingKeywordsLower.end()) {
existingKeywordsLower.insert(toLower(ip));
existingKeywords.push_back(ip);
addedCount++;
}
}
if (addedCount == 0) {
MessageBoxL("所有解析出的 IP 已在上线提醒列表中", "提示", MB_ICONINFORMATION);
return;
}
// Rebuild match pattern
std::string newPattern;
for (const auto& kw : existingKeywords) {
if (!newPattern.empty()) {
newPattern += ";";
}
newPattern += kw;
}
// Update config (preserve existing columnIndex and triggerType)
rule.matchPattern = newPattern;
rule.enabled = true;
if (rule.triggerType == NOTIFY_TRIGGER_NONE) {
rule.triggerType = NOTIFY_TRIGGER_HOST_ONLINE;
}
GetNotifyManager().SetConfig(config);
GetNotifyManager().SaveConfig();
// Build message with SMTP warning if not configured
CString msg;
msg.Format(_TR("已添加 %d 个 IP 到上线提醒列表"), addedCount);
if (!config.smtp.IsValid()) {
msg += _T("\n\n");
msg += _TR("注意: SMTP 未配置,请先在通知设置中配置邮箱");
}
MessageBoxL(msg, _TR("上线提醒"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnMsglogBanIP()
{
// 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("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
return;
}
// Process each extracted IP
int bannedCount = 0;
int alreadyCount = 0;
int whitelistCount = 0;
for (const auto& ip : ipsToAdd) {
// Cannot ban whitelisted IPs
if (IPWhitelist::getInstance().IsWhitelisted(ip)) {
whitelistCount++;
continue;
}
// Already in blacklist
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
alreadyCount++;
continue;
}
IPBlacklist::getInstance().AddIP(ip);
bannedCount++;
}
// Persist to config
if (bannedCount > 0) {
std::string blacklistStr = IPBlacklist::getInstance().Export();
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
}
// Build result message
CString details;
if (bannedCount > 0) {
details.AppendFormat(_TR("已封禁 %d 个 IP"), bannedCount);
}
if (alreadyCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 已在黑名单中"), alreadyCount);
}
if (whitelistCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("%d 个 IP 在白名单中,已跳过"), whitelistCount);
}
if (details.IsEmpty()) {
details = _TR("未执行任何操作");
}
MessageBoxL(details, _TR("封禁IP"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnMsglogUnbanIP()
{
// Get selected log messages and extract IP addresses from them
std::vector<std::string> ipsToProcess;
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
while (pos) {
int row = m_CList_Message.GetNextSelectedItem(pos);
CString msgText = m_CList_Message.GetItemText(row, 2);
CString extractedIP;
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
ipsToProcess.push_back(ipUtf8);
}
}
if (ipsToProcess.empty()) {
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
return;
}
int unblacklistedCount = 0;
int clientRestoredCount = 0;
for (const auto& ip : ipsToProcess) {
// 1. Remove from IP blacklist
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
IPBlacklist::getInstance().RemoveIP(ip);
unblacklistedCount++;
}
// 2. Find all clients matching this IP and restore AUTH_FORBIDDEN to UNAUTHORIZED
for (const auto& kv : m_ClientMap->GetAll()) {
if (strcmp(kv.second.IP, ip.c_str()) == 0
&& kv.second.Authorized == AUTH_FORBIDDEN) {
m_ClientMap->SetClientMapInteger(kv.first, MAP_AUTH, UNAUTHORIZED);
clientRestoredCount++;
}
}
}
// Persist
if (unblacklistedCount > 0) {
std::string blacklistStr = IPBlacklist::getInstance().Export();
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
}
if (clientRestoredCount > 0) {
m_ClientMap->SaveToFile(GetDbPath());
}
// Build result message
CString details;
if (unblacklistedCount > 0) {
details.AppendFormat(_TR("已将 %d 个 IP 从黑名单移除"), unblacklistedCount);
}
if (clientRestoredCount > 0) {
if (!details.IsEmpty()) details += _T("\n");
details.AppendFormat(_TR("已恢复 %d 个客户端的授权状态"), clientRestoredCount);
}
if (unblacklistedCount == 0 && clientRestoredCount == 0) {
details = _TR("未找到需要解除封禁的 IP");
}
MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION);
}
void CMy2015RemoteDlg::OnOnlineAddWatch() void CMy2015RemoteDlg::OnOnlineAddWatch()
{ {
@@ -10483,14 +10991,27 @@ void CMy2015RemoteDlg::OnParamEnableLog()
void CMy2015RemoteDlg::OnParamPrivacyWallpaper() void CMy2015RemoteDlg::OnParamPrivacyWallpaper()
{ {
auto SubMenu = m_MainMenu.GetSubMenu(2);
// 已设置壁纸:再次点击则清空
if (!m_PrivateScreenWallpaper.IsEmpty()) {
m_PrivateScreenWallpaper.Empty();
THIS_CFG.SetStr("settings", "Wallpaper", "");
if (SubMenu)
SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_UNCHECKED);
return;
}
// 未设置壁纸:弹出文件对话框
CFileDialog dlg(TRUE, _T("bmp"), NULL, CFileDialog dlg(TRUE, _T("bmp"), NULL,
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
_T("BMP Files (*.bmp)|*.bmp|All Files (*.*)|*.*||"), this); _T("BMP Files (*.bmp)|*.bmp|All Files (*.*)|*.*||"), this);
if (dlg.DoModal() == IDOK) { if (dlg.DoModal() != IDOK)
return;
CString path = dlg.GetPathName(); CString path = dlg.GetPathName();
// 验证文件是否可以读取
CFile file; CFile file;
if (!file.Open(path, CFile::modeRead | CFile::typeBinary)) { if (!file.Open(path, CFile::modeRead | CFile::typeBinary)) {
MessageBox(_TR("无法读取指定的BMP文件,请检查文件是否存在或权限是否正确。"), MessageBox(_TR("无法读取指定的BMP文件,请检查文件是否存在或权限是否正确。"),
@@ -10498,7 +11019,6 @@ void CMy2015RemoteDlg::OnParamPrivacyWallpaper()
return; return;
} }
// 验证是否是有效的BMP文件
BITMAPFILEHEADER bmfh; BITMAPFILEHEADER bmfh;
if (file.Read(&bmfh, sizeof(bmfh)) != sizeof(bmfh) || bmfh.bfType != 0x4D42) { if (file.Read(&bmfh, sizeof(bmfh)) != sizeof(bmfh) || bmfh.bfType != 0x4D42) {
file.Close(); file.Close();
@@ -10510,14 +11030,11 @@ void CMy2015RemoteDlg::OnParamPrivacyWallpaper()
m_PrivateScreenWallpaper = path; m_PrivateScreenWallpaper = path;
THIS_CFG.SetStr("settings", "Wallpaper", path.GetString()); THIS_CFG.SetStr("settings", "Wallpaper", path.GetString());
auto SubMenu = m_MainMenu.GetSubMenu(2); // 工具菜单 if (SubMenu)
if (SubMenu) {
SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_CHECKED); SubMenu->CheckMenuItem(ID_PARAM_PRIVACY_WALLPAPER, MF_CHECKED);
}
CString msg; CString msg;
msg.Format(_TR("隐私屏幕壁纸已设置为:\n%s"), path); msg.Format(_TR("隐私屏幕壁纸已设置为:\n%s"), path);
MessageBox(msg + CString("\n") + _L("如需恢复默认值,请从磁盘移除该文件。"), _TR("设置成功"), MB_ICONINFORMATION); MessageBox(msg, _TR("设置成功"), MB_ICONINFORMATION);
}
} }
void CMy2015RemoteDlg::OnParamFileV2() void CMy2015RemoteDlg::OnParamFileV2()
@@ -11452,3 +11969,107 @@ void CMy2015RemoteDlg::OnOnlineActiveWnd()
if (!ctx) return; if (!ctx) return;
PostCaptureForegroundWindow(ctx); PostCaptureForegroundWindow(ctx);
} }
bool SetUserEnvironmentVariable(const char* name, const char* value)
{
HKEY hKey = NULL;
LONG ret = RegOpenKeyExA(
HKEY_CURRENT_USER,
"Environment",
0,
KEY_SET_VALUE,
&hKey);
if (ret != ERROR_SUCCESS)
{
return false;
}
ret = RegSetValueExA(
hKey,
name,
0,
REG_SZ, // 或 REG_EXPAND_SZ
(const BYTE*)value,
(DWORD)(strlen(value) + 1));
RegCloseKey(hKey);
if (ret != ERROR_SUCCESS)
{
return false;
}
// 通知系统环境变量已更新
SendMessageTimeoutA(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
(LPARAM)"Environment",
SMTO_ABORTIFHUNG,
5000,
NULL);
return true;
}
void CMy2015RemoteDlg::OnEnableDevDebug()
{
const char* debug = getenv("YAMA_DBG");
if (!debug || strlen(debug) == 0) {
MessageBoxL("您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。",
"提示", MB_ICONINFORMATION);
CInputDialog dlg(this);
dlg.Init(_L("设置环境变量"), "YAMA_DBG:");
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty()) return;
SetUserEnvironmentVariable("YAMA_DBG", dlg.m_str) ?
MessageBoxL("设置成功! 请重启程序和调试器。", "提示", MB_ICONINFORMATION) :
MessageBoxL("设置失败! 通常是权限不足, 请手动在系统环境变量设置。", "提示", MB_ICONINFORMATION);
}
else {
MessageBoxA(_TR("您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n")+
"YAMA_DBG=" + CString(debug), _L("提示"), MB_ICONINFORMATION);
}
}
void CMy2015RemoteDlg::OnOnlineClientLog()
{
BYTE cmd = COMMAND_QUERY_LOG;
SendSelectedCommand(&cmd, sizeof(BYTE));
}
void CMy2015RemoteDlg::OnOnlineHistoryActivity()
{
BYTE cmd = COMMAND_QUERY_ACTIVITY;
SendSelectedCommand(&cmd, sizeof(BYTE));
}
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
{
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
std::string initLog = pInitLog ? *pInitLog : "";
delete pInitLog;
OpenDialog<CClientLog, IDD_DIALOG_CLIENT_LOG>(0, lParam);
CONTEXT_OBJECT* ctx = reinterpret_cast<CONTEXT_OBJECT*>(lParam);
if (ctx && ctx->hDlg && !initLog.empty())
static_cast<CClientLog*>(ctx->hDlg)->AppendLog(initLog);
return 0;
}
void forbiddenClient(context* ctx, void* user) {
_ClientList* m_ClientMap = (_ClientList*)user;
m_ClientMap->SetClientMapInteger(ctx->GetClientID(), MAP_AUTH, AUTH_FORBIDDEN);
}
void CMy2015RemoteDlg::OnOnlineForbidden()
{
if (IDYES != MessageBoxL(_T("确定封禁选定的被控程序吗?"), _T("提示"), MB_ICONQUESTION | MB_YESNO))
return;
BYTE buf[32] = { COMMAND_FORBIDDEN };
SendSelectedCommand(buf, sizeof(buf), forbiddenClient, m_ClientMap);
}
+19 -2
View File
@@ -75,6 +75,7 @@ class CSplashDlg; // 前向声明
class CClientListDlg; class CClientListDlg;
class CLicenseDlg; class CLicenseDlg;
class CSearchBarDlg; class CSearchBarDlg;
class CLogSearchBar;
#include "pwd_gen.h" #include "pwd_gen.h"
@@ -140,7 +141,9 @@ public:
_ClientList *m_ClientMap = nullptr; _ClientList *m_ClientMap = nullptr;
CClientListDlg* m_pClientListDlg = nullptr; CClientListDlg* m_pClientListDlg = nullptr;
CLicenseDlg* m_pLicenseDlg = nullptr; CLicenseDlg* m_pLicenseDlg = nullptr;
CSearchBarDlg* m_pSearchBar = nullptr; // 搜索工具栏 CSearchBarDlg* m_pSearchBar = nullptr;
CLogSearchBar* m_pLogSearchBar = nullptr;
bool m_bLogSearchVisible = false; // 日志搜索条是否显示(右键菜单切换)
BOOL m_bEnableFileV2 = FALSE; // V2 文件传输开关 BOOL m_bEnableFileV2 = FALSE; // V2 文件传输开关
// 构造 // 构造
@@ -170,6 +173,9 @@ protected:
DECLARE_MESSAGE_MAP() DECLARE_MESSAGE_MAP()
public: public:
void SortByColumn(int nColumn); void SortByColumn(int nColumn);
void ApplySort(); // 按偏好重排 m_HostList(调用方需持有 m_cs)
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam) template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam)
@@ -339,6 +345,8 @@ public:
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端) std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射 std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表 std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
std::set<std::string> m_GroupList; std::set<std::string> m_GroupList;
std::string m_selectedGroup; std::string m_selectedGroup;
std::string m_v2KeyPath; // V2 密钥文件路径 std::string m_v2KeyPath; // V2 密钥文件路径
@@ -382,7 +390,7 @@ public:
bool IsDllRequestLimited(const std::string& ip); bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip); void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu; CMenu m_MainMenu;
CBitmap m_bmOnline[60]; CBitmap m_bmOnline[66];
uint64_t m_superID; uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds; std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList; FileTransformCmd m_CmdList;
@@ -560,6 +568,10 @@ public:
afx_msg void OnMsglogDelete(); afx_msg void OnMsglogDelete();
afx_msg void OnMsglogCopy(); afx_msg void OnMsglogCopy();
afx_msg void OnMsglogClear(); afx_msg void OnMsglogClear();
afx_msg void OnMsglogSearch();
afx_msg void OnMsglogLoginNotify();
afx_msg void OnMsglogBanIP();
afx_msg void OnMsglogUnbanIP();
afx_msg void OnOnlineAddWatch(); afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnOnlineRunAsAdmin(); afx_msg void OnOnlineRunAsAdmin();
@@ -636,4 +648,9 @@ public:
afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam);
afx_msg void OnCopyClientInfo(); afx_msg void OnCopyClientInfo();
afx_msg void OnOnlineActiveWnd(); afx_msg void OnOnlineActiveWnd();
afx_msg void OnEnableDevDebug();
afx_msg void OnOnlineClientLog();
afx_msg void OnOnlineHistoryActivity();
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineForbidden();
}; };
@@ -290,6 +290,9 @@
<ClInclude Include="Buffer.h" /> <ClInclude Include="Buffer.h" />
<ClInclude Include="BuildDlg.h" /> <ClInclude Include="BuildDlg.h" />
<ClInclude Include="CClientListDlg.h" /> <ClInclude Include="CClientListDlg.h" />
<ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" />
<ClInclude Include="CDlgFileSend.h" /> <ClInclude Include="CDlgFileSend.h" />
<ClInclude Include="CDrawingBoard.h" /> <ClInclude Include="CDrawingBoard.h" />
<ClInclude Include="CGridDialog.h" /> <ClInclude Include="CGridDialog.h" />
@@ -371,6 +374,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="..\..\client\Audio.cpp" /> <ClCompile Include="..\..\client\Audio.cpp" />
<ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" />
<ClCompile Include="msvc_compat.c"> <ClCompile Include="msvc_compat.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">NotUsing</PrecompiledHeader>
@@ -507,8 +513,11 @@
<Image Include="res\Bitmap\authorize.bmp" /> <Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\Backup.bmp" /> <Image Include="res\Bitmap\Backup.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" /> <Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" />
<Image Include="res\bitmap\compress.bmp" /> <Image Include="res\bitmap\compress.bmp" />
<Image Include="res\Bitmap\Copy.bmp" /> <Image Include="res\Bitmap\Copy.bmp" />
<Image Include="res\Bitmap\Debug.bmp" />
<Image Include="res\Bitmap\delete.bmp" /> <Image Include="res\Bitmap\delete.bmp" />
<Image Include="res\Bitmap\DxgiDesktop.bmp" /> <Image Include="res\Bitmap\DxgiDesktop.bmp" />
<Image Include="res\Bitmap\EditGroup.bmp" /> <Image Include="res\Bitmap\EditGroup.bmp" />
@@ -560,6 +569,7 @@
<Image Include="res\Bitmap\Wallet.bmp" /> <Image Include="res\Bitmap\Wallet.bmp" />
<Image Include="res\Bitmap\WebDesktop.bmp" /> <Image Include="res\Bitmap\WebDesktop.bmp" />
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\ClientLog.ico" />
<Image Include="res\cmdshell.ico" /> <Image Include="res\cmdshell.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\DrawingBoard.ico" /> <Image Include="res\DrawingBoard.ico" />
@@ -84,6 +84,7 @@
<ClCompile Include="PluginSettingsDlg.cpp" /> <ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="PreviewTipWnd.cpp" /> <ClCompile Include="PreviewTipWnd.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" /> <ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="CClientLog.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\..\client\Audio.h" /> <ClInclude Include="..\..\client\Audio.h" />
@@ -188,6 +189,7 @@
<ClInclude Include="PluginSettingsDlg.h" /> <ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" /> <ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="CClientLog.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="2015Remote.rc" /> <ResourceCompile Include="2015Remote.rc" />
@@ -327,5 +329,8 @@
<Image Include="res\bitmap\uninstall.bmp" /> <Image Include="res\bitmap\uninstall.bmp" />
<Image Include="res\Bitmap\Copy.bmp" /> <Image Include="res\Bitmap\Copy.bmp" />
<Image Include="res\bitmap\ActiveWnd.bmp" /> <Image Include="res\bitmap\ActiveWnd.bmp" />
<Image Include="res\Bitmap\Debug.bmp" />
<Image Include="res\ClientLog.ico" />
<Image Include="res\Bitmap\ClientLog.bmp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+20
View File
@@ -30,6 +30,8 @@ enum Index {
IndexLinuxGhost, IndexLinuxGhost,
IndexMacGhost, IndexMacGhost,
IndexAndroidGhost, IndexAndroidGhost,
IndexGhostSystem,
IndexTestRunSystem,
OTHER_ITEM OTHER_ITEM
}; };
@@ -493,6 +495,22 @@ void CBuildDlg::OnBnClickedOk()
szBuffer = ReadResource(IDR_ANDROID_GHOST, dwFileSize, ResFileName::GHOST_ANDROID); szBuffer = ReadResource(IDR_ANDROID_GHOST, dwFileSize, ResFileName::GHOST_ANDROID);
break; break;
} }
case IndexGhostSystem:
file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir);
typ = CLIENT_TYPE_ONE;
startup = Startup_GhostSystem;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break;
case IndexTestRunSystem:
file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir);
typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunSystem;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break;
case OTHER_ITEM: { case OTHER_ITEM: {
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir);
m_OtherItem.GetWindowTextA(file); m_OtherItem.GetWindowTextA(file);
@@ -801,6 +819,8 @@ BOOL CBuildDlg::OnInitDialog()
m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64"); m_ComboExe.InsertStringL(IndexLinuxGhost, "ghost - Linux x64");
m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS"); m_ComboExe.InsertStringL(IndexMacGhost, "ghost - Apple MacOS");
m_ComboExe.InsertStringL(IndexAndroidGhost, "ghost - Google Android"); m_ComboExe.InsertStringL(IndexAndroidGhost, "ghost - Google Android");
m_ComboExe.InsertStringL(IndexGhostSystem, "ghost - SYSTEM");
m_ComboExe.InsertStringL(IndexTestRunSystem, "TestRun - SYSTEM");
m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件")); m_ComboExe.InsertStringL(OTHER_ITEM, CString("选择文件"));
m_ComboExe.SetCurSel(IndexTestRun_MemDLL); m_ComboExe.SetCurSel(IndexTestRun_MemDLL);
+98
View File
@@ -0,0 +1,98 @@
// CActivityDialog.cpp: 历史活动展示对话框(一次性快照,无子连接)
#include "stdafx.h"
#include "afxdialogex.h"
#include "CActivityDialog.h"
#include "resource.h"
#include "../../common/commands.h"
IMPLEMENT_DYNAMIC(CActivityDialog, CDialogEx)
CActivityDialog::CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
{
}
CActivityDialog::~CActivityDialog()
{
}
void CActivityDialog::DoDataExchange(CDataExchange* pDX)
{
CDialogBase::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
}
BEGIN_MESSAGE_MAP(CActivityDialog, CDialogBase)
ON_WM_SIZE()
END_MESSAGE_MAP()
BOOL CActivityDialog::OnInitDialog()
{
CDialogBase::OnInitDialog();
SetWindowText(_TR("历史活动") + " - " + m_IPAddress);
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
SetIcon(hIcon, TRUE);
SetIcon(hIcon, FALSE);
// 用 Segoe UI:MBCS 工程里模板默认字体是 Microsoft Sans Serif(无 Dingbats 字形),
// 显式指定 Segoe UI,中文走字体链接到微软雅黑、Dingbats(✻/✦)走 Segoe UI Symbol 回退。
m_font.CreatePointFont(90, _T("Segoe UI"));
m_editLog.SetFont(&m_font);
// 重建为 Unicode 编辑框:这是 ✻/✦ 能正确显示的关键。重建时已保留上面的字体。
RebuildEdit(m_editLog);
return TRUE;
}
void CActivityDialog::RebuildEdit(CEdit& m_edit)
{
CRect rc;
m_edit.GetWindowRect(&rc);
ScreenToClient(&rc);
DWORD style = m_edit.GetStyle();
DWORD exStyle = m_edit.GetExStyle();
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
UINT ctrlID = m_edit.GetDlgCtrlID();
m_edit.DestroyWindow();
HWND hEdit = ::CreateWindowExW(
exStyle, L"EDIT", L"", style,
rc.left, rc.top, rc.Width(), rc.Height(),
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
AfxGetInstanceHandle(), NULL);
m_edit.Attach(hEdit);
if (hFont)
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
void CActivityDialog::OnSize(UINT nType, int cx, int cy)
{
CDialogBase::OnSize(nType, cx, cy);
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
}
// 快照模式:无子连接,OnReceiveComplete 空实现(纯虚函数必须覆盖)
void CActivityDialog::OnReceiveComplete()
{
}
void CActivityDialog::AppendLog(const std::string& text)
{
if (text.empty()) return;
// \n → \r\n,Windows 编辑框换行需要 \r\n
std::string norm;
norm.reserve(text.size() + 64);
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
norm += '\r';
norm += text[i];
}
// 服务端是 MBCS 工程,编辑框走 A 接口会把 UTF-8 中文当 ANSI 处理导致乱码;
// 与"活动窗口"列一致,用 W 接口显示:先把 UTF-8 转成宽字符,再走 WM_SETTEXTW 写入。
// (编辑框已在 OnInitDialog 中重建为 Unicode 窗口,SetWindowTextW 不会经过 CP_ACP 回转。)
int wlen = MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), NULL, 0);
if (wlen <= 0) return;
std::wstring w(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), &w[0], wlen);
::SetWindowTextW(m_editLog.GetSafeHwnd(), w.c_str());
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
class CActivityDialog : public CDialogBase
{
DECLARE_DYNAMIC(CActivityDialog)
public:
CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
virtual ~CActivityDialog();
virtual void OnReceiveComplete();
void AppendLog(const std::string& text);
protected:
virtual BOOL OnInitDialog();
virtual void DoDataExchange(CDataExchange* pDX);
afx_msg void OnSize(UINT nType, int cx, int cy);
DECLARE_MESSAGE_MAP()
private:
// 把编辑框重建为 Unicode 类窗口(同 CKeyBoardDlg::RebuildEdit)。
// 工程是 MBCS,对话框模板创建的是 ANSI 编辑框,SetWindowTextW 会在 W->A
// 边界用 CP_ACP 转码,Dingbats(✻/✦)等字符会变 ?。重建为 Unicode 后
// W 版消息直通,不再走 CP_ACP。
void RebuildEdit(CEdit& m_edit);
CEdit m_editLog;
CFont m_font; // 编辑框字体(成员持有,随对话框销毁,避免堆泄漏)
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DIALOG_CLIENT_LOG };
#endif
};
+2 -2
View File
@@ -288,7 +288,7 @@ void CClientListDlg::DisplayClients()
CString strLevel; CString strLevel;
strLevel.FormatL(_T("%d"), val.Level); strLevel.FormatL(_T("%d"), val.Level);
CString strAuth = val.Authorized ? _T("Y") : _T("N"); CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN)? _T("Y") : _T("N");
nItem = m_ClientList.InsertItem(nRow, strNo); nItem = m_ClientList.InsertItem(nRow, strNo);
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
@@ -340,7 +340,7 @@ void CClientListDlg::DisplayClients()
CString strLevel; CString strLevel;
strLevel.FormatL(_T("%d"), val.Level); strLevel.FormatL(_T("%d"), val.Level);
CString strAuth = val.Authorized ? _T("Y") : _T("N"); CString strAuth = (val.Authorized == AUTHED_BY_SUPER || val.Authorized == AUTHED_BY_ADMIN) ? _T("Y") : _T("N");
nItem = m_ClientList.InsertItem(nRow, strSubNo); nItem = m_ClientList.InsertItem(nRow, strSubNo);
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
+106
View File
@@ -0,0 +1,106 @@
// CClientLog.cpp: 实现文件
#include "stdafx.h"
#include "afxdialogex.h"
#include "CClientLog.h"
#include "resource.h"
#include "../../common/commands.h"
IMPLEMENT_DYNAMIC(CClientLog, CDialogEx)
CClientLog::CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
{
}
CClientLog::~CClientLog()
{
}
void CClientLog::DoDataExchange(CDataExchange* pDX)
{
CDialogBase::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
}
BEGIN_MESSAGE_MAP(CClientLog, CDialogBase)
ON_WM_DESTROY()
ON_WM_SIZE()
ON_MESSAGE(WM_REPORT_CLIENT_LOG, OnReportLog)
END_MESSAGE_MAP()
BOOL CClientLog::OnInitDialog()
{
CDialogBase::OnInitDialog();
SetWindowText(_TR("客户端日志") + " - " + m_IPAddress);
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
SetIcon(hIcon, TRUE);
SetIcon(hIcon, FALSE);
CFont* pFont = new CFont();
pFont->CreatePointFont(85, _T("Consolas"));
m_editLog.SetFont(pFont);
return TRUE;
}
void CClientLog::OnSize(UINT nType, int cx, int cy)
{
CDialogBase::OnSize(nType, cx, cy);
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
}
void CClientLog::OnDestroy()
{
if (m_ContextObject && m_bConnected) {
BYTE cmd = COMMAND_BYE;
m_ContextObject->Send2Client(&cmd, 1);
}
CDialogBase::OnDestroy();
}
void CClientLog::AppendLog(const std::string& text)
{
if (text.empty()) return;
// \n → \r\n,Windows 编辑框换行需要 \r\n
std::string norm;
norm.reserve(text.size() + 64);
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
norm += '\r';
norm += text[i];
}
// 超过 512KB 时截断旧内容
int existing = m_editLog.GetWindowTextLength();
if (existing > 512 * 1024) {
CString cur;
m_editLog.GetWindowText(cur);
m_editLog.SetWindowText(cur.Mid(cur.GetLength() - 256 * 1024));
}
// 追加到末尾并滚动
CString add = CA2T(norm.c_str(), CP_UTF8);
m_editLog.SetSel(-1, -1);
m_editLog.ReplaceSel(add);
m_editLog.SendMessage(EM_SCROLLCARET);
}
void CClientLog::OnReceiveComplete()
{
auto& buf = m_ContextObject->InDeCompressedBuffer;
if (buf.GetBYTE(0) != TOKEN_REPORT_LOG)
return;
unsigned len = buf.GetBufferLen();
auto* pText = new std::string((char*)(buf.GetBuffer() + 1), len > 1 ? len - 1 : 0);
if (!PostMessage(WM_REPORT_CLIENT_LOG, 0, (LPARAM)pText))
delete pText;
}
LRESULT CClientLog::OnReportLog(WPARAM /*wParam*/, LPARAM lParam)
{
std::string* pText = reinterpret_cast<std::string*>(lParam);
if (pText) {
AppendLog(*pText);
delete pText;
}
return 0;
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
class CClientLog : public CDialogBase
{
DECLARE_DYNAMIC(CClientLog)
public:
CClientLog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
virtual ~CClientLog();
virtual void OnReceiveComplete();
void AppendLog(const std::string& text);
protected:
virtual BOOL OnInitDialog();
virtual void DoDataExchange(CDataExchange* pDX);
afx_msg void OnDestroy();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg LRESULT OnReportLog(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
private:
CEdit m_editLog;
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DIALOG_CLIENT_LOG };
#endif
};
+229
View File
@@ -0,0 +1,229 @@
// LogSearchBar.cpp - 日志列表搜索条实现
#include "stdafx.h"
#include "LogSearchBar.h"
#include "LangManager.h"
BEGIN_MESSAGE_MAP(CLogSearchBar, CWnd)
ON_WM_CREATE()
ON_WM_SIZE()
ON_WM_ERASEBKGND()
ON_WM_TIMER()
ON_EN_CHANGE(IDC_LOG_SEARCH_EDIT, OnEnChangeSearch)
ON_BN_CLICKED(IDC_LOG_SEARCH_PREV, OnBnClickedPrev)
ON_BN_CLICKED(IDC_LOG_SEARCH_NEXT, OnBnClickedNext)
ON_BN_CLICKED(IDC_LOG_SEARCH_CLEAR, OnBnClickedClear)
END_MESSAGE_MAP()
CLogSearchBar::CLogSearchBar()
{
}
CLogSearchBar::~CLogSearchBar()
{
}
BOOL CLogSearchBar::Create(CWnd* pParent, CListCtrl* pList)
{
m_pList = pList;
LPCTSTR cls = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
::LoadCursor(NULL, IDC_ARROW));
return CWnd::Create(cls, _T(""), WS_CHILD | WS_CLIPCHILDREN,
CRect(0, 0, 1, 1), pParent, 0);
}
int CLogSearchBar::OnCreate(LPCREATESTRUCT lpcs)
{
if (CWnd::OnCreate(lpcs) == -1) return -1;
// 搜索输入框
m_editSearch.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_EDIT);
m_editSearch.SetCueBanner(L"搜索日志...", TRUE);
// 清除按钮
m_btnClear.Create(_T("X"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_CLEAR);
// 上一个 / 下一个按钮(系统样式,与浅色背景融合)
m_btnPrev.Create(_T("▲"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_PREV);
m_btnNext.Create(_T("▼"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_FLAT,
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_NEXT);
// 结果计数
m_staticCount.Create(_T(""), WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE,
CRect(0, 0, 1, 1), this, IDC_LOG_SEARCH_COUNT);
// 统一字体:跟父窗口(主对话框)保持一致
CFont* pFont = GetParent()->GetFont();
if (pFont) {
m_staticCount.SetFont(pFont);
m_editSearch.SetFont(pFont);
m_btnClear.SetFont(pFont);
m_btnPrev.SetFont(pFont);
m_btnNext.SetFont(pFont);
}
return 0;
}
void CLogSearchBar::LayoutControls(int cx, int cy)
{
if (!m_editSearch.GetSafeHwnd()) return;
const int m = 2;
const int h = cy - 2 * m;
if (h <= 0) return;
const int btnW = h;
const int cntW = 62;
// 搜索框填满剩余空间(barW 由外部控制总宽,这里保证控件铺满)
const int editW = max(20, cx - 2 * m - 3 * btnW - 2 * m - cntW);
int x = m;
m_staticCount.MoveWindow(x, m, cntW, h);
x += cntW + m;
m_editSearch.MoveWindow(x, m, editW, h);
x += editW + m;
m_btnClear.MoveWindow(x, m, btnW, h);
x += btnW;
m_btnPrev.MoveWindow(x, m, btnW, h);
x += btnW;
m_btnNext.MoveWindow(x, m, btnW, h);
}
void CLogSearchBar::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
LayoutControls(cx, cy);
}
BOOL CLogSearchBar::OnEraseBkgnd(CDC* pDC)
{
CRect rc;
GetClientRect(&rc);
pDC->FillSolidRect(rc, ::GetSysColor(COLOR_3DFACE));
return TRUE;
}
void CLogSearchBar::OnEnChangeSearch()
{
KillTimer(TIMER_SEARCH);
SetTimer(TIMER_SEARCH, SEARCH_DELAY_MS, NULL);
}
void CLogSearchBar::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == TIMER_SEARCH) {
KillTimer(TIMER_SEARCH);
DoSearch();
}
CWnd::OnTimer(nIDEvent);
}
void CLogSearchBar::DoSearch()
{
if (!m_pList || !m_pList->GetSafeHwnd()) return;
CString kw;
m_editSearch.GetWindowText(kw);
m_Results.clear();
m_nCurrentIndex = -1;
if (kw.IsEmpty()) {
UpdateCountText();
return;
}
CString kwLow = kw;
kwLow.MakeLower();
int nCols = m_pList->GetHeaderCtrl() ? m_pList->GetHeaderCtrl()->GetItemCount() : 1;
int nItems = m_pList->GetItemCount();
for (int i = 0; i < nItems; i++) {
for (int c = 0; c < nCols; c++) {
CString cell = m_pList->GetItemText(i, c);
cell.MakeLower();
if (cell.Find(kwLow) >= 0) {
m_Results.push_back(i);
break;
}
}
}
if (!m_Results.empty()) {
m_nCurrentIndex = 0;
GotoMatch(m_nCurrentIndex);
}
UpdateCountText();
}
void CLogSearchBar::GotoMatch(int idx)
{
if (!m_pList || idx < 0 || idx >= (int)m_Results.size()) return;
int item = m_Results[idx];
// 取消所有选中
int n = -1;
while ((n = m_pList->GetNextItem(-1, LVNI_SELECTED)) != -1)
m_pList->SetItemState(n, 0, LVIS_SELECTED | LVIS_FOCUSED);
m_pList->SetItemState(item, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
m_pList->EnsureVisible(item, FALSE);
}
void CLogSearchBar::UpdateCountText()
{
CString text;
if (m_Results.empty()) {
CString kw;
m_editSearch.GetWindowText(kw);
text = kw.IsEmpty() ? _TR("") : _TR("无结果");
} else {
text.Format(_T("%d/%d"), m_nCurrentIndex + 1, (int)m_Results.size());
}
m_staticCount.SetWindowText(text);
}
void CLogSearchBar::InvalidateSearch()
{
KillTimer(TIMER_SEARCH);
m_Results.clear();
m_nCurrentIndex = -1;
UpdateCountText();
if (m_editSearch.GetSafeHwnd()) {
CString kw;
m_editSearch.GetWindowText(kw);
if (!kw.IsEmpty())
SetTimer(TIMER_SEARCH, SEARCH_DELAY_MS, NULL);
}
}
void CLogSearchBar::OnBnClickedPrev()
{
if (m_Results.empty()) return;
m_nCurrentIndex--;
if (m_nCurrentIndex < 0)
m_nCurrentIndex = (int)m_Results.size() - 1;
GotoMatch(m_nCurrentIndex);
UpdateCountText();
}
void CLogSearchBar::OnBnClickedNext()
{
if (m_Results.empty()) return;
m_nCurrentIndex++;
if (m_nCurrentIndex >= (int)m_Results.size())
m_nCurrentIndex = 0;
GotoMatch(m_nCurrentIndex);
UpdateCountText();
}
void CLogSearchBar::OnBnClickedClear()
{
m_editSearch.SetWindowText(_T(""));
m_editSearch.SetFocus();
// 触发 EN_CHANGE → 定时器 → DoSearch 清空结果
}
+57
View File
@@ -0,0 +1,57 @@
// LogSearchBar.h - 日志列表搜索条(嵌入式,固定在右侧)
#pragma once
#include <afxwin.h>
#include <afxcmn.h>
#include <vector>
#define IDC_LOG_SEARCH_EDIT 5001
#define IDC_LOG_SEARCH_PREV 5002
#define IDC_LOG_SEARCH_NEXT 5003
#define IDC_LOG_SEARCH_COUNT 5004
#define IDC_LOG_SEARCH_CLEAR 5005
class CLogSearchBar : public CWnd
{
public:
CLogSearchBar();
virtual ~CLogSearchBar();
// pParent: 主对话框; pList: 要搜索的 CListCtrl(m_CList_Message)
BOOL Create(CWnd* pParent, CListCtrl* pList);
void InvalidateSearch();
// 当前命中行的列表索引,无命中返回 -1(纯内存读)
int GetHighlightedRow() const {
if (m_nCurrentIndex < 0 || m_nCurrentIndex >= (int)m_Results.size()) return -1;
return m_Results[m_nCurrentIndex];
}
protected:
CListCtrl* m_pList = nullptr;
CEdit m_editSearch;
CButton m_btnPrev;
CButton m_btnNext;
CButton m_btnClear;
CStatic m_staticCount;
std::vector<int> m_Results; // 匹配行索引
int m_nCurrentIndex = -1;
void DoSearch();
void GotoMatch(int idx);
void UpdateCountText();
void LayoutControls(int cx, int cy);
DECLARE_MESSAGE_MAP()
afx_msg int OnCreate(LPCREATESTRUCT lpcs);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
afx_msg void OnTimer(UINT_PTR nIDEvent);
afx_msg void OnEnChangeSearch();
afx_msg void OnBnClickedPrev();
afx_msg void OnBnClickedNext();
afx_msg void OnBnClickedClear();
static const UINT TIMER_SEARCH = 1;
static const UINT SEARCH_DELAY_MS = 400;
};
+8 -3
View File
@@ -104,9 +104,14 @@ inline const char* GetColumnName(int index) {
"Camera", // 6 "Camera", // 6
"RTT", // 7 "RTT", // 7
"Version", // 8 "Version", // 8
"InstallTime", // 9 "InstallTime", // 9 ONLINELIST_INSTALLTIME
"ActiveWindow", // 10 "ActiveWindow", // 10 ONLINELIST_LOGINTIME
"ClientType", // 11 "ClientType", // 11 ONLINELIST_CLIENTTYPE
"Path", // 12 ONLINELIST_PATH
"PublicIP", // 13 ONLINELIST_PUBIP
"StartTime", // 14 ONLINELIST_STARTTIME
"Capabilities", // 15 ONLINELIST_CAPABILITIES
"Privilege", // 16 ONLINELIST_PRIVILEGE
}; };
if (index >= 0 && index < sizeof(names)/sizeof(names[0])) { if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
return names[index]; return names[index];
+37 -20
View File
@@ -141,35 +141,52 @@ bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword, c
} }
} }
// Get column text (for COMPUTER_NAME column, prefer remark if available) // Split pattern by semicolon and get keywords
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
// Helper: check if any keyword matches a given column text
auto matchColumn = [&keywords, this](const CString& colText) -> std::string {
if (colText.IsEmpty()) return "";
std::string colTextStr = CT2A(colText, CP_UTF8);
std::string colLower = colTextStr;
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
for (const auto& kw : keywords) {
std::string trimmed = Trim(kw);
if (trimmed.empty()) continue;
std::string kwLower = trimmed;
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
if (colLower.find(kwLower) != std::string::npos) {
return trimmed;
}
}
return "";
};
// Check primary column (for COMPUTER_NAME column, prefer remark if available)
CString colText; CString colText;
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) { if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
colText = remark; colText = remark;
} else { } else {
colText = ctx->GetClientData(rule.columnIndex); colText = ctx->GetClientData(rule.columnIndex);
} }
if (colText.IsEmpty()) return false; std::string matched = matchColumn(colText);
if (!matched.empty()) {
// Convert to std::string for matching outMatchedKeyword = matched;
std::string colTextStr = CT2A(colText, CP_UTF8);
// Split pattern by semicolon and check each keyword
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
for (const auto& kw : keywords) {
std::string trimmed = Trim(kw);
if (trimmed.empty()) continue;
// Case-insensitive substring search
std::string colLower = colTextStr;
std::string kwLower = trimmed;
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
if (colLower.find(kwLower) != std::string::npos) {
outMatchedKeyword = trimmed;
m_config.lastNotifyTime[clientId] = now; m_config.lastNotifyTime[clientId] = now;
return true; return true;
} }
// Also check the IP column (ONLINELIST_IP) so IP keywords added from log
// messages can match against host IP addresses when they come online.
CString ipText = ctx->GetClientData(ONLINELIST_IP);
matched = matchColumn(ipText);
if (!matched.empty()) {
outMatchedKeyword = matched;
m_config.lastNotifyTime[clientId] = now;
return true;
} }
return false; return false;
+13 -12
View File
@@ -102,18 +102,19 @@ void NotifySettingsDlg::PopulateComboBoxes()
// Column combo - use translatable strings // Column combo - use translatable strings
m_comboNotifyColumn.ResetContent(); m_comboNotifyColumn.ResetContent();
struct { int id; const TCHAR* name; } columns[] = { struct { int id; const TCHAR* name; } columns[] = {
{ 0, _T("IP地址") }, { ONLINELIST_IP, _T("IP地址") },
{ 1, _T("地址") }, { ONLINELIST_ADDR, _T("地址") },
{ 2, _T("地理位置") }, { ONLINELIST_LOCATION, _T("地理位置") },
{ 3, _T("计算机名") }, { ONLINELIST_COMPUTER_NAME,_T("计算机名") },
{ 4, _T("操作系统") }, { ONLINELIST_OS, _T("操作系统") },
{ 5, _T("CPU") }, { ONLINELIST_CPU, _T("CPU") },
{ 6, _T("摄像头") }, { ONLINELIST_VIDEO, _T("摄像头") },
{ 7, _T("延迟") }, { ONLINELIST_PING, _T("延迟") },
{ 8, _T("版本") }, { ONLINELIST_VERSION, _T("版本") },
{ 9, _T("安装时间") }, { ONLINELIST_INSTALLTIME, _T("安装时间") },
{ 10, _T("活动窗口") }, { ONLINELIST_LOGINTIME, _T("活动窗口") },
{ 11, _T("客户端类型") }, { ONLINELIST_CLIENTTYPE, _T("客户端类型") },
{ ONLINELIST_PRIVILEGE, _T("权限") },
}; };
for (const auto& col : columns) { for (const auto& col : columns) {
CString item; CString item;
+18 -7
View File
@@ -754,7 +754,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
SysMenu->CheckMenuItem(IDM_REMOTE_CURSOR, m_Settings.RemoteCursor ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_REMOTE_CURSOR, m_Settings.RemoteCursor ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ENABLE_SSE2, m_Settings.CpuSpeedup == 1 ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_ENABLE_SSE2, m_Settings.CpuSpeedup == 1 ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_FAST_STRETCH, GetFastStretchMode() ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_FAST_STRETCH, GetFastStretchMode() ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_Settings.CustomCursor ? MF_CHECKED : MF_UNCHECKED);
CMenu fpsMenu; CMenu fpsMenu;
if (fpsMenu.CreatePopupMenu()) { if (fpsMenu.CreatePopupMenu()) {
@@ -919,6 +919,15 @@ BOOL CScreenSpyDlg::OnInitDialog()
m_ContextObject->Send2Client(sizeCmd, 10); m_ContextObject->Send2Client(sizeCmd, 10);
} }
} }
std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
BYTE bToken[1 + sizeof(SignatureResp)] = { COMMAND_SCREEN_SIGNATURE };
std::string msg = ToPekingTimeAsString(0);
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
SignatureResp resp = { 0 };
memcpy(resp.msg, msg.data(), msg.size());
memcpy(resp.signature, signature.data(), signature.size());
memcpy(bToken + 1, &resp, sizeof(SignatureResp));
m_ContextObject->Send2Client(bToken, sizeof(bToken));
SendNext(); SendNext();
@@ -1359,7 +1368,7 @@ VOID CScreenSpyDlg::OnReceiveComplete()
m_dwCustomCursorHash = hash; m_dwCustomCursorHash = hash;
// 如果当前正在使用自定义光标,立即更新显示 // 如果当前正在使用自定义光标,立即更新显示
if (m_bCursorIndex == 254 && m_bIsCtrl && m_bUseCustomCursor) { if (m_bCursorIndex == 254 && m_bIsCtrl && m_Settings.CustomCursor) {
if (m_Settings.RemoteCursor) { if (m_Settings.RemoteCursor) {
// RemoteCursor 模式:触发重绘以显示新光标 // RemoteCursor 模式:触发重绘以显示新光标
Invalidate(FALSE); Invalidate(FALSE);
@@ -1439,7 +1448,7 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构 if (m_bIsCtrl && !m_bIsTraceCursor) {//替换指定窗口所属类的WNDCLASSEX结构
HCURSOR cursor; HCURSOR cursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标 if (m_bCursorIndex == 254) { // -2: 使用自定义光标
cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW); cursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头 } else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
cursor = LoadCursor(NULL, IDC_ARROW); cursor = LoadCursor(NULL, IDC_ARROW);
} else { } else {
@@ -1903,7 +1912,7 @@ void CScreenSpyDlg::OnPaint()
// 2. 获取光标句柄(支持自定义光标) // 2. 获取光标句柄(支持自定义光标)
HCURSOR hCursor; HCURSOR hCursor;
if (m_bCursorIndex == 254) { // -2: 使用自定义光标 if (m_bCursorIndex == 254) { // -2: 使用自定义光标
hCursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW); hCursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
} else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头 } else if (m_bCursorIndex == 255) { // -1: 不支持,回退到箭头
hCursor = LoadCursor(NULL, IDC_ARROW); hCursor = LoadCursor(NULL, IDC_ARROW);
} else { } else {
@@ -2177,17 +2186,19 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
break; break;
} }
case IDM_CUSTOM_CURSOR: { case IDM_CUSTOM_CURSOR: {
m_bUseCustomCursor = !m_bUseCustomCursor; m_Settings.CustomCursor = m_Settings.CustomCursor ? FALSE : TRUE;
SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_bUseCustomCursor ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_CUSTOM_CURSOR, m_Settings.CustomCursor ? MF_CHECKED : MF_UNCHECKED);
// 如果当前是自定义光标,立即更新显示 // 如果当前是自定义光标,立即更新显示
if (m_bCursorIndex == 254 && m_bIsCtrl) { if (m_bCursorIndex == 254 && m_bIsCtrl) {
HCURSOR cursor = (m_bUseCustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW); HCURSOR cursor = (m_Settings.CustomCursor && m_hCustomCursor) ? m_hCustomCursor : LoadCursor(NULL, IDC_ARROW);
if (m_Settings.RemoteCursor) { if (m_Settings.RemoteCursor) {
Invalidate(FALSE); Invalidate(FALSE);
} else { } else {
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)cursor); SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)cursor);
} }
} }
BYTE cmd[4] = { CMD_CUSTOM_CURSOR, m_Settings.CustomCursor };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
break; break;
} }
-1
View File
@@ -277,7 +277,6 @@ public:
HCURSOR m_hRemoteCursor = NULL; HCURSOR m_hRemoteCursor = NULL;
HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标 HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标
DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希 DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希
BOOL m_bUseCustomCursor = TRUE; // 是否使用自定义光标
CRect m_CRect; CRect m_CRect;
double m_wZoom=1, m_hZoom=1; double m_wZoom=1, m_hZoom=1;
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素) int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
+65 -4
View File
@@ -56,6 +56,7 @@ BEGIN_MESSAGE_MAP(CSystemDlg, CDialog)
ON_COMMAND(ID_PLIST_INJECT, &CSystemDlg::OnPlistInject) ON_COMMAND(ID_PLIST_INJECT, &CSystemDlg::OnPlistInject)
ON_COMMAND(ID_PLIST_ANTI_BLACK_SCREEN, &CSystemDlg::OnPlistAntiBlackScreen) ON_COMMAND(ID_PLIST_ANTI_BLACK_SCREEN, &CSystemDlg::OnPlistAntiBlackScreen)
ON_COMMAND(ID_WLIST_VIEW, &CSystemDlg::OnWlistView) ON_COMMAND(ID_WLIST_VIEW, &CSystemDlg::OnWlistView)
ON_NOTIFY(NM_DBLCLK, IDC_LIST_SYSTEM, &CSystemDlg::OnNMDblclkListSystem)
END_MESSAGE_MAP() END_MESSAGE_MAP()
@@ -72,9 +73,14 @@ BOOL CSystemDlg::OnInitDialog()
m_bHow==TOKEN_PSLIST m_bHow==TOKEN_PSLIST
? str.FormatL("%s - 进程管理", m_IPAddress) ? str.FormatL("%s - 进程管理", m_IPAddress)
:str.FormatL("%s - 窗口管理", m_IPAddress); :str.FormatL("%s - 窗口管理", m_IPAddress);
#ifdef _WIN32
if(m_bHow == TOKEN_WSLIST)
str += CString(" - ") + _TR("双击查看");
#endif
SetWindowText(str);//设置对话框标题 SetWindowText(str);//设置对话框标题
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT); m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
LoadSortPreference(); // 加载上次记忆的排序偏好
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表 if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180); m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70); m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
@@ -222,6 +228,8 @@ void CSystemDlg::ShowWindowsList(void)
lvc.pszText = str.GetBuffer(0); lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(1, &lvc); m_ControlList.SetColumn(1, &lvc);
ApplySort(); // 应用记忆的排序偏好
} }
void CSystemDlg::ShowProcessList(void) void CSystemDlg::ShowProcessList(void)
@@ -255,8 +263,8 @@ void CSystemDlg::ShowProcessList(void)
m_ControlList.SetItemText(i, 1, str); m_ControlList.SetItemText(i, 1, str);
m_ControlList.SetItemText(i, 2, szProcessFullPath); m_ControlList.SetItemText(i, 2, szProcessFullPath);
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str()); m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
// ItemData 为进程ID // ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath}, arr[1].c_str() }; auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() };
m_ControlList.SetItemData(i, DWORD_PTR(data)); m_ControlList.SetItemData(i, DWORD_PTR(data));
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环 dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
@@ -268,6 +276,8 @@ void CSystemDlg::ShowProcessList(void)
lvc.pszText = str.GetBuffer(0); lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程 m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
ApplySort(); // 应用记忆的排序偏好
} }
@@ -314,8 +324,6 @@ int CALLBACK CSystemDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM
void CSystemDlg::SortByColumn(int nColumn) void CSystemDlg::SortByColumn(int nColumn)
{ {
static int m_nSortColumn = 0;
static bool m_bSortAscending = false;
if (nColumn == m_nSortColumn) { if (nColumn == m_nSortColumn) {
// 如果点击的是同一列,切换排序顺序 // 如果点击的是同一列,切换排序顺序
m_bSortAscending = !m_bSortAscending; m_bSortAscending = !m_bSortAscending;
@@ -325,11 +333,56 @@ void CSystemDlg::SortByColumn(int nColumn)
m_bSortAscending = true; m_bSortAscending = true;
} }
ApplySort();
SaveSortPreference();
}
void CSystemDlg::ApplySort()
{
// 无有效排序偏好时保持列表原始顺序
if (m_nSortColumn < 0) {
return;
}
// 防御:配置被手动修改导致列索引越界时跳过排序
CHeaderCtrl* pHeader = m_ControlList.GetHeaderCtrl();
if (pHeader && m_nSortColumn >= pHeader->GetItemCount()) {
return;
}
// 创建排序信息 // 创建排序信息
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending); std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo)); m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
} }
void CSystemDlg::LoadSortPreference()
{
// 进程管理 / 窗口管理 分别记忆各自的排序偏好
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
std::string val = THIS_CFG.GetStr("list", key, "");
if (val.empty()) {
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
m_bSortAscending = true;
return;
}
auto arr = StringToVector(val, ',');
if (arr.size() >= 2) {
m_nSortColumn = atoi(arr[0].c_str());
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
} else {
m_nSortColumn = -1;
m_bSortAscending = true;
}
}
void CSystemDlg::SaveSortPreference()
{
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
THIS_CFG.SetStr("list", key, val);
}
void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult) void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{ {
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR); LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
@@ -666,3 +719,11 @@ void CSystemDlg::OnWlistView()
delete[] arg; delete[] arg;
} }
} }
void CSystemDlg::OnNMDblclkListSystem(NMHDR* pNMHDR, LRESULT* pResult)
{
if (m_bHow == TOKEN_WSLIST)
OnWlistView();
*pResult = 0;
}
+6
View File
@@ -19,6 +19,8 @@ public:
void OnReceiveComplete(void); void OnReceiveComplete(void);
CWnd* m_pParent; CWnd* m_pParent;
BOOL m_bHow; BOOL m_bHow;
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
// 对话框数据 // 对话框数据
enum { IDD = IDD_DIALOG_SYSTEM }; enum { IDD = IDD_DIALOG_SYSTEM };
@@ -32,6 +34,9 @@ public:
void DeleteAllItems(); void DeleteAllItems();
void SortByColumn(int nColumn); void SortByColumn(int nColumn);
void ApplySort();
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
@@ -49,4 +54,5 @@ public:
afx_msg void OnPlistInject(); afx_msg void OnPlistInject();
afx_msg void OnPlistAntiBlackScreen(); afx_msg void OnPlistAntiBlackScreen();
afx_msg void OnWlistView(); afx_msg void OnWlistView();
afx_msg void OnNMDblclkListSystem(NMHDR* pNMHDR, LRESULT* pResult);
}; };
+1 -1
View File
@@ -46,7 +46,7 @@
// 程序版本号 [建议格式: X.Y.Z] // 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏 // 影响:关于对话框、标题栏
#define BRAND_VERSION "1.3.7" #define BRAND_VERSION "1.3.8"
// 启动画面名称 [建议大写,更有 Logo 感] // 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染) // 影响:启动画面 Logo 文字(大号艺术字体渲染)
+37 -2
View File
@@ -1814,6 +1814,16 @@ bool CWebService::StartRemoteDesktop(uint64_t device_id) {
context* ctx = m_pParentDlg->FindHost(device_id); context* ctx = m_pParentDlg->FindHost(device_id);
if (!ctx) return false; if (!ctx) return false;
// Cancel any pending grace-period close so the existing session stays alive.
{
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
auto it = m_PendingCloseTimers.find(device_id);
if (it != m_PendingCloseTimers.end()) {
it->second->store(true);
m_PendingCloseTimers.erase(it);
}
}
// Check if there's already a Web session for this device // Check if there's already a Web session for this device
// Only reuse if Web has already triggered AND a Web dialog exists // Only reuse if Web has already triggered AND a Web dialog exists
// This ensures MFC and Web have independent dialogs // This ensures MFC and Web have independent dialogs
@@ -1854,12 +1864,37 @@ void CWebService::StopRemoteDesktop(uint64_t device_id) {
} }
} }
// If no more web clients watching, close only the Web session dialog // If no more web clients watching, defer close by 30 s so a quick
// MFC dialogs remain open // mobile reconnect (e.g. switching to a TOTP app) reuses the existing
// CScreenSpyDlg without a full cold-start.
if (watchingCount == 0) { if (watchingCount == 0) {
auto cancelled = std::make_shared<std::atomic<bool>>(false);
{
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
auto it = m_PendingCloseTimers.find(device_id);
if (it != m_PendingCloseTimers.end())
it->second->store(true);
m_PendingCloseTimers[device_id] = cancelled;
}
std::thread([this, device_id, cancelled]() {
std::this_thread::sleep_for(std::chrono::seconds(30));
if (cancelled->load()) return;
{
std::lock_guard<std::mutex> lk(m_PendingCloseMutex);
m_PendingCloseTimers.erase(device_id);
}
int count = 0;
{
std::lock_guard<std::mutex> lock(m_ClientsMutex);
for (const auto& [ws, client] : m_Clients)
if (client.watch_device_id == device_id) count++;
}
if (count == 0 && m_pParentDlg) {
ClearWebTriggered(device_id); ClearWebTriggered(device_id);
m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id); m_pParentDlg->CloseWebRemoteDesktopByClientID(device_id);
} }
}).detach();
}
} }
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
+9
View File
@@ -5,6 +5,8 @@
#include <vector> #include <vector>
#include <map> #include <map>
#include <set> #include <set>
#include <atomic>
#include <chrono>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <memory> #include <memory>
@@ -231,6 +233,13 @@ private:
std::set<uint64_t> m_OfflineDevices; // Devices that went offline since last flush std::set<uint64_t> m_OfflineDevices; // Devices that went offline since last flush
std::mutex m_DirtyDevicesMutex; std::mutex m_DirtyDevicesMutex;
// Grace-period timers: delay CloseWebRemoteDesktopByClientID by 30 s after
// the last web viewer disconnects to allow quick mobile reconnects (e.g.
// switching to a TOTP app briefly). Cancelled by StartRemoteDesktop when a
// viewer reconnects within the window.
std::map<uint64_t, std::shared_ptr<std::atomic<bool>>> m_PendingCloseTimers;
std::mutex m_PendingCloseMutex;
public: public:
// Check if a device session was triggered by web (should be hidden) // Check if a device session was triggered by web (should be hidden)
bool IsWebTriggered(uint64_t device_id); bool IsWebTriggered(uint64_t device_id);
+1
View File
@@ -21,6 +21,7 @@ enum {
ONLINELIST_PUBIP, ONLINELIST_PUBIP,
ONLINELIST_STARTTIME, ONLINELIST_STARTTIME,
ONLINELIST_CAPABILITIES, // 客户端能力位 ONLINELIST_CAPABILITIES, // 客户端能力位
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
ONLINELIST_MAX, ONLINELIST_MAX,
}; };
+33
View File
@@ -1945,3 +1945,36 @@ FRPC Զ
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=Sub-license limit for %s has been cleared.\nTakes effect when the sub-client re-authenticates. 授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=Sub-license limit for %s has been cleared.\nTakes effect when the sub-client re-authenticates.
提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=Note: APK has been patched and the original signature is invalid. Re-sign before installing. 提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=Note: APK has been patched and the original signature is invalid. Re-sign before installing.
重签命令 (需 Android SDK build-tools):=Re-sign command (requires Android SDK build-tools): 重签命令 (需 Android SDK build-tools):=Re-sign command (requires Android SDK build-tools):
权限=Access
双击查看=Double Click to View
浏览窗口=View Windows
开发者模式=Developer Mode
您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。=Only if YAMA_DBG is set that you can debug this program!\r\nPlease contact administrator!
您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n=You have set YAMA_DBG, please ensure it's correct!\r\n
设置环境变量=Set YAMA_DBG
设置成功! 请重启程序和调试器。=Successful! Please restart program and debugger.
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
运行日志=Client Log
客户端日志=Client Log
历史活动=Activity History
封禁使用=Forbidden Client
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
无结果=Not found
严禁=Prohibited
禁止=Forbidden
失败=Failed
搜索日志=Search Logs
隐藏搜索=Hide Search
请=Please
所选日志消息中未检测到有效的 IP 地址=No valid IP address detected in the selected log messages
所有解析出的 IP 已在上线提醒列表中=All extracted IPs are already in the login notify list
已添加 %d 个 IP 到上线提醒列表=Added %d IP(s) to login notify list
封禁IP=Ban IP
已封禁 %d 个 IP=Banned %d IP(s)
%d 个 IP 已在黑名单中=%d IP(s) already in blacklist
%d 个 IP 在白名单中,已跳过=%d IP(s) in whitelist, skipped
未执行任何操作=No action taken
解除封禁=Unban IP
已将 %d 个 IP 从黑名单移除=Removed %d IP(s) from blacklist
已恢复 %d 个客户端的授权状态=Restored authorization for %d client(s)
未找到需要解除封禁的 IP=No IPs found that need unbanning
+33
View File
@@ -1936,3 +1936,36 @@ FRPC Զ
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=授权 %s 的下级数量限制已清除。\n下级重新认证后生效。 授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=授权 %s 的下级数量限制已清除。\n下级重新认证后生效。
提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=提示: APK 已被修改,原簽名失效,需要重簽後才能安裝到裝置。 提示: APK 已被修改,原签名失效,需要重签后才能安装到设备。=提示: APK 已被修改,原簽名失效,需要重簽後才能安裝到裝置。
重签命令 (需 Android SDK build-tools):=重簽命令 (需 Android SDK build-tools): 重签命令 (需 Android SDK build-tools):=重簽命令 (需 Android SDK build-tools):
权限=权限
双击查看=双击查看
浏览窗口=浏览窗口
开发者模式=开发者模式
您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。=您必须正确设置 YAMA_DBG 才能调试此程序!\r\n请联系管理员请求授权。
您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n=您已设置 YAMA_DBG, 请确保环境变量正确无误!\r\n
设置环境变量=设置环境变量
设置成功! 请重启程序和调试器。=设置成功! 请重启程序和调试器。
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
运行日志=运行日志
客户端日志=客户端日志
历史活动=歷史活動
封禁使用=封禁使用
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
无结果=无结果
严禁=严禁
禁止=禁止
失败=失败
搜索日志=搜索日志
隐藏搜索=隐藏搜索
请=请
所有解析出的 IP 已在上线提醒列表中=所有解析出的 IP 已在上線提醒列表中
已添加 %d 个 IP 到上线提醒列表=已新增 %d 個 IP 到上線提醒列表
所选日志消息中未检测到有效的 IP 地址=所選日誌消息中未檢測到有效的 IP 位址
封禁IP=封禁IP
已封禁 %d 个 IP=已封禁 %d 個 IP
%d 个 IP 已在黑名单中=%d 個 IP 已在黑名單中
%d 个 IP 在白名单中,已跳过=%d 個 IP 在白名單中,已跳過
未执行任何操作=未執行任何操作
解除封禁=解除封禁
已将 %d 个 IP 从黑名单移除=已將 %d 個 IP 從黑名單移除
已恢复 %d 个客户端的授权状态=已恢復 %d 個客戶端的授權狀態
未找到需要解除封禁的 IP=未找到需要解除封禁的 IP
Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+25 -2
View File
@@ -271,6 +271,15 @@
#define IDB_BITMAP_ACTIVE_WND 390 #define IDB_BITMAP_ACTIVE_WND 390
#define IDR_BINARY8 391 #define IDR_BINARY8 391
#define IDR_ANDROID_GHOST 391 #define IDR_ANDROID_GHOST 391
#define IDB_BITMAP_DEBUG 392
#define IDD_DIALOG8 393
#define IDD_DIALOG_CLIENT_LOG 393
#define IDB_BITMAP_CLIENTLOG 395
#define IDI_CLIENTLOG 396
#define IDB_BITMAP_FORBIDDEN 397
#define IDB_BITMAP_HISTORY_ACTIVITY 398
#define IDB_BITMAP_VIEW_WND 399
#define IDB_BITMAP_PROXY_AUTORUN 400
#define IDC_MESSAGE 1000 #define IDC_MESSAGE 1000
#define IDC_ONLINE 1001 #define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002 #define IDC_STATIC_TIPS 1002
@@ -754,6 +763,7 @@
#define IDC_LIST_TRIGGERS 2543 #define IDC_LIST_TRIGGERS 2543
#define IDC_STATIC_TRIGGER_TYPE 2544 #define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545 #define IDC_STATIC_TRIGGER_ACTION 2545
#define IDC_EDIT_LOG 2546
#define ID_ONLINE_UPDATE 32772 #define ID_ONLINE_UPDATE 32772
#define ID_ONLINE_MESSAGE 32773 #define ID_ONLINE_MESSAGE 32773
#define ID_ONLINE_DELETE 32775 #define ID_ONLINE_DELETE 32775
@@ -983,6 +993,10 @@
#define ID_MSGLOG_CLEAR 33041 #define ID_MSGLOG_CLEAR 33041
#define ID_CANCEL_SHARE 33042 #define ID_CANCEL_SHARE 33042
#define ID_MSGLOG_COPY 33043 #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_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045 #define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_33046 33046 #define ID_33046 33046
@@ -1009,14 +1023,23 @@
#define ID_ONLINE_33066 33066 #define ID_ONLINE_33066 33066
#define ID_ONLINE_ACTIVE_WND 33067 #define ID_ONLINE_ACTIVE_WND 33067
#define ID_LICENSE_SET_LIMIT 33068 #define ID_LICENSE_SET_LIMIT 33068
#define ID_33069 33069
#define ID_ONLINE_VIEW_WND 33070
#define ID_33071 33071
#define ID_ENABLE_DEV_DEBUG 33072
#define ID_ONLINE_33073 33073
#define ID_ONLINE_CLIENT_LOG 33074
#define ID_ONLINE_33075 33075
#define ID_ONLINE_FORBIDDEN 33076
#define ID_ONLINE_HISTORY_ACTIVITY 33082
#define ID_EXIT_FULLSCREEN 40001 #define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 392 #define _APS_NEXT_RESOURCE_VALUE 401
#define _APS_NEXT_COMMAND_VALUE 33069 #define _APS_NEXT_COMMAND_VALUE 33083
#define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif
+2
View File
@@ -110,6 +110,8 @@
#define WM_SPLITTER_MOVED WM_USER+3040 #define WM_SPLITTER_MOVED WM_USER+3040
#define WM_SPLITTER_RELEASED WM_USER+3041 #define WM_SPLITTER_RELEASED WM_USER+3041
#define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面 #define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面
#define WM_OPENCLIENTLOGDIALOG WM_USER+3043 // 打开客户端日志对话框
#define WM_REPORT_CLIENT_LOG WM_USER+3044 // 追加日志到已打开的对话框
#ifdef _UNICODE #ifdef _UNICODE
#if defined _M_IX86 #if defined _M_IX86
+32 -1
View File
@@ -95,6 +95,12 @@ type wsHub struct {
mu sync.RWMutex mu sync.RWMutex
clients map[*wsClient]struct{} clients map[*wsClient]struct{}
// screenCloseTimers delays CloseScreen by screenCloseGrace after the last
// viewer disconnects. A reconnect within that window cancels the timer and
// reuses the live screen sub-conn (hot path), avoiding a full cold-start.
// Guarded by mu.
screenCloseTimers map[string]*time.Timer
unsub func() unsub func()
// Hardening knobs wired from server.Config. Nil/empty values mean // Hardening knobs wired from server.Config. Nil/empty values mean
@@ -112,6 +118,7 @@ func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger)
devices: devices, devices: devices,
log: log, log: log,
clients: make(map[*wsClient]struct{}), clients: make(map[*wsClient]struct{}),
screenCloseTimers: make(map[string]*time.Timer),
} }
h.unsub = devices.Subscribe(h) h.unsub = devices.Subscribe(h)
return h return h
@@ -405,7 +412,7 @@ func (h *wsHub) unregister(c *wsClient) {
// session so the device stops encoding. Done OUTSIDE the lock so the // session so the device stops encoding. Done OUTSIDE the lock so the
// hub's mutators can take their own locks without risk of recursion. // hub's mutators can take their own locks without risk of recursion.
if c.watching != "" && h.countWatchers(c.watching) == 0 { if c.watching != "" && h.countWatchers(c.watching) == 0 {
h.devices.CloseScreen(c.watching) h.deferredCloseScreen(c.watching)
} }
// Terminal sessions are single-viewer by design, so any open session // Terminal sessions are single-viewer by design, so any open session
// belongs to this client. Tear it down so the next viewer doesn't // belongs to this client. Tear it down so the next viewer doesn't
@@ -421,6 +428,30 @@ func (h *wsHub) unregister(c *wsClient) {
c.close() c.close()
} }
// screenCloseGrace is how long we keep a device's screen sub-conn alive after
// the last browser viewer disconnects. A reconnect within this window (e.g.
// the user briefly switched to a TOTP app on mobile) cancels the timer and
// resumes on the hot path — no cold-start, no waiting for a new IDR.
const screenCloseGrace = 30 * time.Second
// deferredCloseScreen schedules CloseScreen after screenCloseGrace unless a
// viewer reconnects first. Must be called without h.mu held.
func (h *wsHub) deferredCloseScreen(deviceID string) {
h.mu.Lock()
if t, ok := h.screenCloseTimers[deviceID]; ok {
t.Stop()
}
h.screenCloseTimers[deviceID] = time.AfterFunc(screenCloseGrace, func() {
h.mu.Lock()
delete(h.screenCloseTimers, deviceID)
h.mu.Unlock()
if h.countWatchers(deviceID) == 0 {
h.devices.CloseScreen(deviceID)
}
})
h.mu.Unlock()
}
// ----- HTTP handler ------------------------------------------------------- // ----- HTTP handler -------------------------------------------------------
func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) { func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) {
+6 -1
View File
@@ -244,7 +244,7 @@ func (h *wsHub) handleDisconnect(c *wsClient, _ []byte) {
h.mu.Unlock() h.mu.Unlock()
c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`)) c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`))
if prev != "" && h.countWatchers(prev) == 0 { if prev != "" && h.countWatchers(prev) == 0 {
h.devices.CloseScreen(prev) h.deferredCloseScreen(prev)
} }
} }
@@ -288,6 +288,11 @@ func (h *wsHub) handleConnect(c *wsClient, raw []byte) {
c.queueBinary(cache.Keyframe) c.queueBinary(cache.Keyframe)
} }
h.mu.Lock() h.mu.Lock()
// Cancel any pending deferred close so the relay stays alive.
if t, ok := h.screenCloseTimers[in.ID]; ok {
t.Stop()
delete(h.screenCloseTimers, in.ID)
}
c.watching = in.ID c.watching = in.ID
h.mu.Unlock() h.mu.Unlock()
return return
+24 -2
View File
@@ -4185,11 +4185,33 @@
clearTimeout(backgroundDisconnectTimer); clearTimeout(backgroundDisconnectTimer);
backgroundDisconnectTimer = null; backgroundDisconnectTimer = null;
} }
// Reconnect or send immediate ping const screenPage = document.getElementById('screen-page');
const onScreenPage = screenPage && screenPage.classList.contains('active') && currentDevice;
if (!ws || ws.readyState !== WebSocket.OPEN) { if (!ws || ws.readyState !== WebSocket.OPEN) {
// WS was dropped — reset decoder before reconnect.
if (onScreenPage && decoder) {
try { decoder.close(); } catch(e) {}
decoder = null;
needKeyframe = true;
}
connectWebSocket();
} else if (onScreenPage && isTouchDevice) {
// iOS suspends the tab and silently invalidates the VideoDecoder
// GPU context (or quietly closes the WS while readyState stays OPEN).
// Always force a clean WS reconnect on mobile so the server delivers
// a fresh stream start; the server no longer sends a stale cached
// keyframe (cleared on disconnect), so the client waits for the next
// natural IDR — no mosaic, no freeze.
if (decoder) {
try { decoder.close(); } catch(e) {}
decoder = null;
needKeyframe = true;
}
ws.onclose = null;
ws.close();
connectWebSocket(); connectWebSocket();
} else if (token) { } else if (token) {
// Connection still open - send immediate ping to refresh server heartbeat
ws.send(JSON.stringify({ cmd: 'ping', token })); ws.send(JSON.stringify({ cmd: 'ping', token }));
} }
} }