12 Commits

Author SHA1 Message Date
yuanyuanxiang
837d89c8b5 Feat: Sub-license count limit - LicenseLimit field in licenses.ini + context menu
- Add LicenseLimit field to LicenseInfo struct (0 = not set, unlimited)
- Add GetLicenseLimit/SetLicenseLimit: read/write LicenseLimit key in licenses.ini
- Append |lic:N to reserved field in TOKEN_AUTH response only when
  LicenseLimit > 0; absent |lic: means no limit (client defaults to 9999),
  so super admin authenticating to its own server is never falsely terminated
- Add "Sub-license limit" item in CLicenseDlg right-click menu (1-9999,
  empty = clear limit); menu label shows current value in real time
- Limit change takes effect when sub-client re-authenticates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 13:01:35 +02:00
yuanyuanxiang
c1433b4b5d Fix: Restore memory DLL at client program startup 2026-06-20 12:38:28 +02:00
yuanyuanxiang
71963b740b Fix: Authorization client use different keyboard log directory 2026-06-20 12:38:28 +02:00
yuanyuanxiang
5b37df26fd Opt: improve adaptive-size rendering 2026-06-20 12:38:28 +02:00
yuanyuanxiang
851fed4739 Feat: sign TOKEN_AUTH response and add TOKEN_SERVER_VERIFY to prevent fake server
TOKEN_AUTH: when the server has a V2 private key, signs "SN|valid(0/1)"
with ECDSA P-256 and places "sig:<base64>" in the response reserved field.
Clients can verify server identity without changing the request format.

TOKEN_SERVER_VERIFY (251): added constant to commands.h; handler already
present in 2015RemoteDlg.cpp for the challenge-response server identity check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:38:28 +02:00
yuanyuanxiang
103123f533 Feat: anti-cracking hardening - version binding and updated libs 2026-06-20 12:38:20 +02:00
yuanyuanxiang
66c950cecb Refactor: split ScanScreen window-capture into 3 private methods 2026-06-16 21:03:45 +02:00
yuanyuanxiang
91d4c0a523 Fix: eliminate extra screen restarts on connection init
Two changes to reduce unnecessary CScreenSpy restarts when connecting:

1. Client (ScreenManager.cpp): Initialize CScreenSpy with bitrate from
   the locally-saved quality profile, so CMD_QUALITY_LEVEL arriving from
   the server (same bitrate as default) hits SetBitRate(3000)==3000 and
   skips the restart instead of comparing against the hard-coded 0.
   Also fixes QualityLevel init to use the already-computed `quality`
   variable (which honours the QUALITY_DISABLED override when algo!=NUL)
   rather than re-reading the cfg key a second time.

2. Server (ScreenSpyDlg.cpp): Only send CMD_SCREEN_SIZE strategy=2 when
   the session is in QUALITY_ADAPTIVE mode and a cached maxWidth exists.
   Fixed quality levels already carry resolution via CMD_QUALITY_PROFILES,
   so unconditionally sending CMD_SCREEN_SIZE caused a second restart when
   the screen spy was still rebuilding from the first one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:14:06 +02:00
yuanyuanxiang
abafd673a2 Opt: skip 8MB raw first-frame in H264 mode; server unlocks on first IDR instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:29:59 +02:00
yuanyuanxiang
2765d95950 Feat: Support viewing active window via online-host popup menu 2026-06-15 19:15:31 +02:00
yuanyuanxiang
931492a294 Fix: clamp ARGBToNV12 dims to even-aligned ctx width/height
to prevent heap overflow on odd-sized windows
2026-06-15 19:14:49 +02:00
yuanyuanxiang
d3b9e7faae Feat: window capture via PrintWindow with server-side HWND routing by clientID 2026-06-15 14:11:42 +02:00
40 changed files with 763 additions and 93 deletions

View File

@@ -210,9 +210,10 @@ int CFFmpegH264Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
uint32_t width, uint32_t height,
int direction)
{
int signed_height = direction * (int)height;
int w = (int)width;
int h = (int)height;
// Clamp to encoder's even-aligned frame dimensions (same reason as encode()).
int w = m_ctx->width;
int h = m_ctx->height;
int signed_height = direction * h;
int y_size = w * h;
int uv_size = (w / 2) * (h / 2);
m_i420Scratch.resize(y_size + 2 * uv_size);
@@ -249,8 +250,12 @@ int CFFmpegH264Encoder::encode(
if (!m_ctx || !m_frame || !m_packet) return -1;
if (av_frame_make_writable(m_frame) < 0) return -1;
int w = (int)width;
int h = (int)height;
// Use the encoder's even-aligned dimensions, not the raw passed-in values.
// m_ctx->width/height = p.width & ~1, m_frame is allocated for exactly those
// dimensions. If we pass an odd width/height, ARGBToNV12 writes one extra row
// past the end of m_frame->data[0] → heap corruption / access violation.
int w = m_ctx->width;
int h = m_ctx->height;
int signed_height = direction * h;
if (bpp == 32) {

View File

@@ -12,6 +12,8 @@ extern "C" {
#include "ServiceWrapper.h"
}
extern void licenseInit();
// Check if CPU supports AVX2 instruction set
static BOOL IsAVX2Supported()
{
@@ -214,6 +216,8 @@ int main(int argc, const char *argv[])
return -1;
}
licenseInit();
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
InitWindowsService(NewService(
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
@@ -338,6 +342,7 @@ BOOL APIENTRY DllMain( HINSTANCE hInstance,
"CPU 不兼容", MB_ICONERROR);
return FALSE;
}
licenseInit();
g_MyApp.g_hInstance = (HINSTANCE)hInstance;
CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL));
break;
@@ -566,6 +571,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string expiredDate;
BOOL isAuthKernel = IsAuthKernel(expiredDate);
if (isAuthKernel) ParseAuthServer(&settings);
Mprintf("[StartClient] Current client: %s\n", isAuthKernel ? "AUTH" : "NORMAL");
std::string pubIP = cfg.GetStr("settings", "public_ip", "");
// V2 authorization supports offline mode, verify signature and skip timeout check
VERIFY_V2_AND_SET_AUTHORIZED();
@@ -584,7 +590,8 @@ DWORD WINAPI StartClient(LPVOID lParam)
}
app.SetThreadRun(TRUE);
ThreadInfo* kb = CreateKB(&settings, bExit, pubIP);
ThreadInfo* kb = CreateKB(&settings, bExit, pubIP, isAuthKernel);
static auto _ = RestoreMemDLL(&cfg, &settings, app.g_bExit);
while (app.m_bIsRunning(&app)) {
ULONGLONG dwTickCount = GetTickCount64();
if (!ClientObject->ConnectServer(settings.ServerIP(), settings.ServerPort())) {

View File

@@ -342,6 +342,7 @@ protected:
void *m_main = NULL;
public:
BOOL m_isAuth = FALSE;
std::string m_LoginMsg; // 登录消息摘要
std::string m_LoginSignature; // 登录消息签名
};

View File

@@ -50,11 +50,12 @@ IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string&
return NULL;
}
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP)
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP, BOOL isAuth)
{
ThreadInfo *tKeyboard = new ThreadInfo();
tKeyboard->run = FOREVER_RUN;
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
sub->m_isAuth = isAuth;
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
tKeyboard->p = sub;
tKeyboard->conn = conn;
@@ -80,11 +81,6 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb;
// C2C 初始化
if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
static int n = RestoreMemDLL();
if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
}
}
BOOL IsThreadsRunning(ThreadInfo* threads, int count)
@@ -280,10 +276,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
}
else {
else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r || (time(0)-start < 15)) {
if (This && (r || (time(0)-start < 15))) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
@@ -304,10 +300,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit);
}
else {
else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
}
if (r || (time(0)-start < 15)) {
if (This && (r || (time(0)-start < 15))) {
char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf);
@@ -331,7 +327,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
sprintf_s(buf, "Inject %s to process [%d] %s", info.Name, info.Pid ? info.Pid : ret, ret ? "succeed" : "failed");
Mprintf("%s\n", buf);
ClientMsg msg("代码注入", buf);
This->SendData((LPBYTE)&msg, sizeof(msg));
if (This)This->SendData((LPBYTE)&msg, sizeof(msg));
}
SAFE_DELETE(dll);
SAFE_DELETE(runner);
@@ -648,8 +644,9 @@ std::string getHardwareIDByCfg(std::string& pwdHash, const std::string& masterHa
return "";
}
int CKernelManager::RestoreMemDLL() {
std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile *m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This) {
binFile bin(CLIENT_PATH);
std::map<std::string, std::vector<BYTE>> m_MemDLL;
// 枚举所有以 .md5 结尾的值名称
auto md5Keys = m_cfg->EnumValues("settings", ".md5");
@@ -707,7 +704,7 @@ int CKernelManager::RestoreMemDLL() {
if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400);
PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf);
BYTE* data = m_MemDLL[md5].data();
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, this), 0, NULL));
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, This), 0, NULL));
// 更新注册表中的运行时状态
// 如果有时间间隔限制,更新 LastRunTime
@@ -727,7 +724,7 @@ int CKernelManager::RestoreMemDLL() {
}
}
return count;
return m_MemDLL;
}
template<typename T = DllExecuteInfo>

View File

@@ -25,7 +25,7 @@
// 根据配置决定采用什么通讯协议
IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, bool exit_while_disconnect = false);
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP);
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, BOOL isAuth = FALSE);
class ActivityWindow
{
@@ -184,7 +184,6 @@ public:
uint64_t m_MyClientID = 0;
// 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg)
{
m_LoginMsg = msg;
@@ -295,4 +294,6 @@ public:
}
};
std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile* m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This = NULL);
#endif // !defined(AFX_KERNELMANAGER_H__B1186DC0_E4D7_4D1A_A8B8_08A01B87B89E__INCLUDED_)

View File

@@ -52,10 +52,8 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
clip::set_error_handler(NULL);
#endif
m_bIsOfflineRecord = offline;
CKernelManager* main = (CKernelManager*)pClient->GetMain();
BOOL isAuth = main ? main->IsAuthKernel() : FALSE;
char path[MAX_PATH] = { "C:\\Windows\\" };
if (!isAuth) GetModuleFileNameA(NULL, path, sizeof(path));
if (!pClient->m_isAuth) GetModuleFileNameA(NULL, path, sizeof(path));
std::string fileName = GetExeHashStr() + ".db";
GET_FILEPATH(path, fileName.c_str());
strcpy_s(m_strRecordFile, path);

View File

@@ -170,6 +170,8 @@ public:
// 感兴趣区域 (ROI)
RECT m_ROI = {0,0,0,0};
bool m_bNeedRestart = false; // 捕获对象需要重建(如窗口尺寸变化)
HWND m_NextTargetWnd = NULL; // 重建时应切换的目标窗口NULL=保持原 HWND
int m_nScaleSendWidth = 0;
int m_nScaleSendHeight = 0;
@@ -859,6 +861,7 @@ public:
void ensureEncoder(int width, int height)
{
if (m_encoder) return;
if (width < 2 || height < 2) return; // x264 做偶数对齐 &~11→0触发内部 strdup 泄漏;< 2 一并拦住
EncoderRequest req;
req.width = width;
req.height = height;

View File

@@ -31,6 +31,9 @@
#include <audioclient.h>
#include <functiondiscoverykeys_devpkey.h>
#include <cstdint>
extern "C" uint32_t licenseGetBuildTag() { volatile uint32_t tag = 0xC0DE2026u; return tag; }
bool IsWindows8orHigher()
{
typedef LONG(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
@@ -153,7 +156,7 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
m_ScreenSettings.FullScreen = cfg.GetInt("settings", "FullScreen", priv);
m_ScreenSettings.RemoteCursor = cfg.GetInt("settings", "RemoteCursor", 0);
m_ScreenSettings.ScrollDetectInterval = cfg.GetInt("settings", "ScrollDetectInterval", 2); // 默认每2帧
m_ScreenSettings.QualityLevel = cfg.GetInt("settings", "QualityLevel", quality);
m_ScreenSettings.QualityLevel = quality;
m_ScreenSettings.CpuSpeedup = cfg.GetInt("settings", "CpuSpeedup", 0);
m_ScreenSettings.AudioEnabled = cfg.GetInt("settings", "AudioEnabled", 0); // 默认禁用音频
m_ScreenSettings.EncodeLevel = cfg.GetInt("settings", "EncodeLevel", LEVEL_H264_SOFT);
@@ -471,6 +474,19 @@ void CScreenManager::InitScreenSpy()
DXGI = param->buffer[0];
algo = param->length > 1 ? param->buffer[1] : algo;
all = param->length > 2 ? param->buffer[2] : all;
// buffer[3..10]: HWND(uint64_t),启动时直接指定窗口捕获;
// 值为 (uint64_t)-1 时进入动态前景模式,每帧自动跟踪当前前景窗口
if (param->length >= 3 + (int)sizeof(uint64_t) && !m_hTargetWnd && !m_bDynamicForeground) {
uint64_t hwnd64 = 0;
memcpy(&hwnd64, param->buffer + 3, sizeof(uint64_t));
if (hwnd64 == (uint64_t)-1) {
m_bDynamicForeground = true;
m_hTargetWnd = GetForegroundWindow();
Mprintf("CScreenManager: 动态前景窗口模式,初始 HWND=%p\n", m_hTargetWnd);
} else if (hwnd64) {
m_hTargetWnd = (HWND)(UINT_PTR)hwnd64;
}
}
}
m_pUserParam = param;
} else {
@@ -481,6 +497,11 @@ void CScreenManager::InitScreenSpy()
if (level >= 0 && level < QUALITY_COUNT) {
algo = m_QualityProfiles[level].algorithm;
}
// 窗口捕获必须走 GDIPrintWindowScreenCapturerDXGI 无窗口捕获能力
if ((m_hTargetWnd || m_bDynamicForeground) && DXGI == USING_DXGI) {
DXGI = USING_GDI;
Mprintf("CScreenManager: 窗口捕获模式,强制 GDI\n");
}
// 保存屏幕类型,服务端用于判断是否显示虚拟桌面相关菜单
m_ScreenSettings.ScreenType = DXGI;
Mprintf("CScreenManager: Type %d Algorithm: %d (QualityLevel=%d)\n", DXGI, int(algo), level);
@@ -531,12 +552,16 @@ void CScreenManager::InitScreenSpy()
} else {
SAFE_DELETE(s);
m_isGDI = TRUE;
m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen);
m_ScreenSpyObject = new CScreenSpy(32, algo, FALSE, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen, m_hTargetWnd, m_bDynamicForeground);
Mprintf("CScreenManager: DXGI SPY init failed!!! Using GDI instead.\n");
}
} else {
m_isGDI = TRUE;
m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen);
m_ScreenSpyObject = new CScreenSpy(32, algo, DXGI == USING_VIRTUAL, DEFAULT_GOP, all, m_ScreenSettings.EncodeLevel, rect, switchScreen, m_hTargetWnd, m_bDynamicForeground);
}
// 用已保存的质量配置初始化码率,避免 CMD_QUALITY_LEVEL 到达时 0→3000 触发不必要重启
if (m_ScreenSpyObject && level >= 0 && level < QUALITY_COUNT) {
m_ScreenSpyObject->SetBitRate(m_QualityProfiles[level].bitRate);
}
}
@@ -640,11 +665,15 @@ DWORD WINAPI CScreenManager::WorkThreadProc(LPVOID lParam)
if (!This->IsConnected() && This->m_bIsWorking) This->OnReconnect();
if (!This->IsConnected()) continue;
if (!This->m_SendFirst && This->IsConnected()) {
// 窗口捕获模式下目标窗口最小化时跳过首帧,等窗口恢复后再发
HWND _targetWnd = This->m_ScreenSpyObject ? This->m_ScreenSpyObject->GetTargetWindow() : NULL;
if (!_targetWnd || !IsIconic(_targetWnd)) {
This->m_SendFirst = TRUE;
This->SendBitMapInfo();
Sleep(50);
This->SendFirstScreen();
}
}
// 降低桌面检查频率避免频繁的DC重置导致闪屏
if (This->IsRunAsService() && !This->m_virtual) {
auto now = clock();
@@ -698,6 +727,16 @@ DWORD WINAPI CScreenManager::WorkThreadProc(LPVOID lParam)
}
This->SendNextScreen(szBuffer, ulNextSendLength);
}
// 窗口捕获:尺寸变化时在工作线程内原地重建,无需跨线程同步
if (This->m_ScreenSpyObject && This->m_ScreenSpyObject->m_bNeedRestart) {
// 动态前景模式切换了新窗口:把新 HWND 同步回 CScreenManager
// 否则 InitScreenSpy 会沿用旧 HWND导致无限重建循环
if (This->m_ScreenSpyObject->m_NextTargetWnd)
This->m_hTargetWnd = This->m_ScreenSpyObject->m_NextTargetWnd;
SAFE_DELETE(This->m_ScreenSpyObject);
This->InitScreenSpy();
This->m_SendFirst = FALSE; // 触发重发 BitmapInfo + 首帧
}
}
timeEndPeriod(1);
Mprintf("ScreenWorkThread Exit\n");
@@ -831,6 +870,33 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
}
break;
}
case COMMAND_SCREEN_WINDOW: {
// [mode:1][data] — mode=0x00 按标题, mode=0x01 按 HWND(uint64_t), 其余=恢复全屏
BYTE mode = (ulLength > 1) ? szBuffer[1] : 0xFF;
if (mode == 0x00 && ulLength > 2) {
char title[512] = {};
int titleLen = ulLength - 2;
if (titleLen > (int)sizeof(title) - 1) titleLen = (int)sizeof(title) - 1;
memcpy(title, szBuffer + 2, titleLen);
m_hTargetWnd = title[0] ? FindWindowA(NULL, title) : NULL;
Mprintf("[CScreenManager] 窗口捕获(标题): '%s' -> HWND=%p\n", title, m_hTargetWnd);
} else if (mode == 0x01 && ulLength >= 2 + sizeof(uint64_t)) {
uint64_t val = 0;
memcpy(&val, szBuffer + 2, sizeof(uint64_t));
m_hTargetWnd = (HWND)(UINT_PTR)val;
Mprintf("[CScreenManager] 窗口捕获(HWND): 0x%llx -> HWND=%p\n", val, m_hTargetWnd);
} else {
m_hTargetWnd = NULL;
m_bDynamicForeground = false;
// 防止 RestartScreen→InitScreenSpy 重新读取旧 HWND 再次进入窗口模式
if (m_pUserParam && m_pUserParam->length >= 3 + (int)sizeof(uint64_t)) {
memset(m_pUserParam->buffer + 3, 0, sizeof(uint64_t));
}
Mprintf("[CScreenManager] 窗口捕获取消,恢复全屏\n");
}
RestartScreen();
break;
}
case COMMAND_ENCODE_LEVEL: {
int encodeLevel = szBuffer[1];
iniFile cfg(CLIENT_PATH);
@@ -2047,9 +2113,13 @@ VOID CScreenManager::ProcessCommand(LPBYTE szBuffer, ULONG ulLength)
}
}
// 窗口捕获模式:只能查看,不能控制
if (m_ScreenSpyObject && m_ScreenSpyObject->GetTargetWindow()) {
return;
// 窗口捕获模式:点击前先将目标窗口置前,确保 SendInput 落到正确的窗口上
if (m_ScreenSpyObject) {
HWND hwndTarget = m_ScreenSpyObject->GetTargetWindow();
if (hwndTarget && IsWindow(hwndTarget) && !IsIconic(hwndTarget)) {
if (!SetForegroundWindow(hwndTarget))
return; // UIPI 等原因置前失败,不向错误窗口注入输入
}
}
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {

View File

@@ -64,6 +64,8 @@ public:
BOOL m_bIsWorking;
BOOL m_bIsBlockInput;
RECT m_ROI = {0};
HWND m_hTargetWnd = NULL; // 窗口捕获目标NULL=全屏)
bool m_bDynamicForeground = false; // true=每帧自动跟踪前景窗口HWND sentinel=-1触发
BOOL m_SwitchScreen = TRUE;
BOOL SendClientClipboard(BOOL fast);
VOID UpdateClientClipboard(char *szBuffer, ULONG ulLength);

View File

@@ -13,11 +13,34 @@
//////////////////////////////////////////////////////////////////////
CScreenSpy::CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk, int gop, BOOL all, int level,
RECT rc, BOOL switchScreen) :
RECT rc, BOOL switchScreen, HWND hwnd, bool dynamicFg) :
ScreenCapture(ulbiBitCount, algo, all, level, rc, switchScreen)
{
m_hTargetWnd = hwnd;
m_bDynamicForeground = dynamicFg;
m_GOP = gop;
// 窗口捕获模式:用 DWM 真实边界覆盖基类的全屏尺寸,并缓存阴影偏移量
if (hwnd) {
RECT wndRc = {}, frameRc = {};
GetWindowRect(hwnd, &wndRc);
if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRc, sizeof(frameRc)))) {
m_ShadowLeft = frameRc.left - wndRc.left;
m_ShadowTop = frameRc.top - wndRc.top;
} else {
frameRc = wndRc;
}
m_ulFullWidth = frameRc.right - frameRc.left;
m_ulFullHeight = frameRc.bottom - frameRc.top;
m_CachedWndW = wndRc.right - wndRc.left;
m_CachedWndH = wndRc.bottom - wndRc.top;
m_iScreenX = frameRc.left; // 窗口左上角在屏幕上的绝对坐标,供 PointConversion 使用
m_iScreenY = frameRc.top;
m_bZoomed = false;
Mprintf("CScreenSpy: 窗口捕获 HWND=%p 尺寸=%dx%d shadow=(%d,%d)\n",
hwnd, m_ulFullWidth, m_ulFullHeight, m_ShadowLeft, m_ShadowTop);
}
m_BitmapInfor_Full = ConstructBitmapInfo(ulbiBitCount, m_ulFullWidth, m_ulFullHeight);
iniFile cfg(CLIENT_PATH);
@@ -122,10 +145,19 @@ CScreenSpy::~CScreenSpy()
LPBYTE CScreenSpy::GetFirstScreenData(ULONG* ulFirstScreenLength)
{
if (m_hTargetWnd && IsIconic(m_hTargetWnd)) {
*ulFirstScreenLength = 0;
return nullptr;
}
ScanScreen(m_hFullMemDC, m_hDeskTopDC, m_ulFullWidth, m_ulFullHeight);
m_RectBuffer[0] = TOKEN_FIRSTSCREEN;
LPBYTE bmp = scaleBitmap(m_BmpZoomBuffer, (LPBYTE)m_BitmapData_Full);
memcpy(m_FirstBuffer, bmp, m_BitmapInfor_Send->bmiHeader.biSizeImage);
// H264/AV1不发原始位图IDR 到达后服务端自行解锁;节省每次切窗口的流量峰值
if (m_bAlgorithm == ALGORITHM_H264) {
*ulFirstScreenLength = 0;
return nullptr;
}
memcpy(1 + m_RectBuffer, bmp, m_BitmapInfor_Send->bmiHeader.biSizeImage);
if (m_bAlgorithm == ALGORITHM_GRAY) {
ToGray(1 + m_RectBuffer, 1 + m_RectBuffer, m_BitmapInfor_Send->bmiHeader.biSizeImage);
@@ -138,13 +170,18 @@ LPBYTE CScreenSpy::GetFirstScreenData(ULONG* ulFirstScreenLength)
VOID CScreenSpy::ScanScreen(HDC hdcDest, HDC hdcSour, ULONG ulWidth, ULONG ulHeight)
{
if (m_hTargetWnd || m_bDynamicForeground) {
if (m_bDynamicForeground && !UpdateDynamicForeground()) return;
if (IsIconic(m_hTargetWnd)) return;
if (!CheckWindowResize()) return;
CaptureWindowContent(hdcDest, hdcSour, ulWidth, ulHeight);
return;
}
if (m_bVirtualPaint) {
// 先用深色填充背景,避免窗口移动时留下残影
RECT rcFill = { 0, 0, (LONG)ulWidth, (LONG)ulHeight };
HBRUSH hBrush = CreateSolidBrush(RGB(30, 30, 30)); // 深灰色背景
HBRUSH hBrush = CreateSolidBrush(RGB(30, 30, 30));
FillRect(hdcDest, &rcFill, hBrush);
DeleteObject(hBrush);
int n = 0;
if (n = EnumWindowsTopToDown(NULL, EnumHwndsPrint, (LPARAM)&m_data.SetScreenDC(hdcDest))) {
Mprintf("EnumWindowsTopToDown failed: %d!!! GetLastError: %d\n", n, GetLastError());
@@ -158,16 +195,97 @@ VOID CScreenSpy::ScanScreen(HDC hdcDest, HDC hdcSour, ULONG ulWidth, ULONG ulHei
#else
const ULONG ulJumpLine = 50;
const ULONG ulJumpSleep = ulJumpLine / 10;
for (int i = 0, ulToJump = 0; i < ulHeight; i += ulToJump) {
for (int i = 0, ulToJump = 0; i < (int)ulHeight; i += ulToJump) {
ULONG ulv1 = ulHeight - i;
if (ulv1 > ulJumpLine)
ulToJump = ulJumpLine;
else
ulToJump = ulv1;
BitBlt(hdcDest, 0, i, ulWidth, ulToJump, hdcSour,0, i, SRCCOPY);
ulToJump = (ulv1 > ulJumpLine) ? ulJumpLine : ulv1;
BitBlt(hdcDest, 0, i, ulWidth, ulToJump, hdcSour, 0, i, SRCCOPY);
Sleep(ulJumpSleep);
}
#endif
}
// 每帧跟踪前景窗口;切换时尺寸相同直接复用,尺寸不同触发重建
// 返回 false 表示本帧跳过(冻结)
bool CScreenSpy::UpdateDynamicForeground()
{
HWND fg = GetForegroundWindow();
if (fg && fg != m_hTargetWnd) {
RECT wndRc = {}, frameRc = {};
GetWindowRect(fg, &wndRc);
frameRc = wndRc;
DwmGetWindowAttribute(fg, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRc, sizeof(frameRc));
ULONG newW = (ULONG)(frameRc.right - frameRc.left);
ULONG newH = (ULONG)(frameRc.bottom - frameRc.top);
if (newW != m_ulFullWidth || newH != m_ulFullHeight) {
// 尺寸不同:让 WorkThread 重建m_NextTargetWnd 传递新 HWND
m_NextTargetWnd = fg;
m_bNeedRestart = true;
return false;
}
// 尺寸相同:直接切换,更新阴影偏移缓存,无需重建
m_ShadowLeft = frameRc.left - wndRc.left;
m_ShadowTop = frameRc.top - wndRc.top;
m_CachedWndW = wndRc.right - wndRc.left;
m_CachedWndH = wndRc.bottom - wndRc.top;
m_PendingWndW = m_PendingWndH = 0;
m_hTargetWnd = fg;
Mprintf("CScreenSpy: 前景切换(同尺寸) -> HWND=%p\n", fg);
}
return m_hTargetWnd != NULL; // NULL=无前景窗口,冻结上一帧
}
// 检测窗口 resizeGetWindowRect 每帧调用DWM 查询仅在尺寸稳定 300ms 后触发
// 同时更新 m_iScreenX/Y窗口移动时坐标同步返回 false 表示本帧跳过
bool CScreenSpy::CheckWindowResize()
{
static const DWORD RESIZE_DEBOUNCE_MS = 300;
RECT fullRc = {};
GetWindowRect(m_hTargetWnd, &fullRc);
m_iScreenX = fullRc.left + m_ShadowLeft;
m_iScreenY = fullRc.top + m_ShadowTop;
int w = fullRc.right - fullRc.left;
int h = fullRc.bottom - fullRc.top;
if (w == m_CachedWndW && h == m_CachedWndH)
return true;
// 尺寸有变化:更新防抖记录
if (w != m_PendingWndW || h != m_PendingWndH) {
m_PendingWndW = w;
m_PendingWndH = h;
m_SizeChangeTick = GetTickCount();
}
if (GetTickCount() - m_SizeChangeTick < RESIZE_DEBOUNCE_MS)
return false; // 尚未稳定,冻结等待
// 稳定 300ms查询 DWM 真实帧边界,决定是否重建
RECT frameRc = fullRc;
if (SUCCEEDED(DwmGetWindowAttribute(m_hTargetWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRc, sizeof(frameRc)))) {
m_ShadowLeft = frameRc.left - fullRc.left;
m_ShadowTop = frameRc.top - fullRc.top;
} else {
m_ShadowLeft = m_ShadowTop = 0;
}
if ((ULONG)(frameRc.right - frameRc.left) != m_ulFullWidth ||
(ULONG)(frameRc.bottom - frameRc.top) != m_ulFullHeight) {
Mprintf("CScreenSpy: 窗口尺寸变化 %dx%d -> %dx%d触发重建\n",
m_ulFullWidth, m_ulFullHeight,
frameRc.right - frameRc.left, frameRc.bottom - frameRc.top);
m_bNeedRestart = true;
return false;
}
m_CachedWndW = w;
m_CachedWndH = h;
return true;
}
// 前景模式直接 BitBlt零闪烁指定窗口模式用 PrintWindow处理遮挡
void CScreenSpy::CaptureWindowContent(HDC hdcDest, HDC hdcSour, ULONG ulWidth, ULONG ulHeight)
{
if (m_bDynamicForeground) {
BitBlt(hdcDest, 0, 0, ulWidth, ulHeight, hdcSour, m_iScreenX, m_iScreenY, SRCCOPY);
} else {
HDC hTmp = m_data.GetWindowDC();
HBITMAP hOld = (HBITMAP)SelectObject(hTmp, m_data.GetWindowBmp());
if (PrintWindow(m_hTargetWnd, hTmp, PW_RENDERFULLCONTENT))
BitBlt(hdcDest, 0, 0, ulWidth, ulHeight, hTmp, m_ShadowLeft, m_ShadowTop, SRCCOPY);
SelectObject(hTmp, hOld);
}
}

View File

@@ -95,10 +95,25 @@ protected:
BOOL m_bVirtualPaint;// 是否虚拟绘制
EnumHwndsPrintData m_data;
HWND m_hTargetWnd = NULL; // 窗口捕获目标NULL=全屏)
bool m_bDynamicForeground = false; // true=每帧跟踪前景窗口(由 sentinel=-1 触发)
int m_ShadowLeft = 0; // DWM 阴影左偏移frameRc.left - fullRc.left帧间缓存
int m_ShadowTop = 0; // DWM 阴影上偏移frameRc.top - fullRc.top帧间缓存
int m_CachedWndW = 0; // 上帧 GetWindowRect 宽度,用于检测 resize 无需每帧调 DWM
int m_CachedWndH = 0; // 上帧 GetWindowRect 高度
int m_PendingWndW = 0; // 防抖:检测到尺寸变化后记录的新宽度
int m_PendingWndH = 0; // 防抖:检测到尺寸变化后记录的新高度
DWORD m_SizeChangeTick = 0; // 防抖尺寸上次变化的时间戳GetTickCount
public:
CScreenSpy(ULONG ulbiBitCount, BYTE algo, BOOL vDesk = FALSE, int gop = DEFAULT_GOP, BOOL all = FALSE,
int level = LEVEL_H264_SOFT, RECT rc = {0}, BOOL switchScreen = TRUE);
int level = LEVEL_H264_SOFT, RECT rc = {0}, BOOL switchScreen = TRUE, HWND hwnd = NULL, bool dynamicFg = false);
virtual HWND GetTargetWindow() const override { return m_hTargetWnd; }
// 窗口模式下 m_ulFullWidth/Height 是窗口尺寸,但 SendInput 的 dx/dy 分母必须是屏幕尺寸
virtual int GetScreenWidth() const override { return m_hTargetWnd ? GetSystemMetrics(SM_CXSCREEN) : m_ulFullWidth; }
virtual int GetScreenHeight() const override { return m_hTargetWnd ? GetSystemMetrics(SM_CYSCREEN) : m_ulFullHeight; }
virtual ~CScreenSpy();
@@ -251,6 +266,14 @@ public:
m_hDeskTopDC = GetDC(NULL);
m_data.Create(m_hDeskTopDC, m_iScreenX, m_iScreenY, m_ulFullWidth, m_ulFullHeight);
}
private:
// 前景跟踪:切换或尺寸变化时更新 m_hTargetWnd返回 false 表示本帧冻结
bool UpdateDynamicForeground();
// 防抖 + 重建检测:更新 m_iScreenX/Y返回 false 表示本帧冻结
bool CheckWindowResize();
// 执行实际像素拷贝BitBlt 或 PrintWindow
void CaptureWindowContent(HDC hdcDest, HDC hdcSour, ULONG ulWidth, ULONG ulHeight);
};
#endif // !defined(AFX_SCREENSPY_H__5F74528D_9ABD_404E_84D2_06C96A0615F4__INCLUDED_)

View File

@@ -68,6 +68,9 @@ bool CX264Encoder::open(int width, int height, int fps, int crf)
bool CX264Encoder::open(x264_param_t * param)
{
// x264_encoder_open 在 0×0 时已完成 x264_param_strdup 才报错,需在此拦截
if (param->i_width < 2 || param->i_height < 2) return false;
m_pPicIn = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));
m_pPicOut = (x264_picture_t*)calloc(1, sizeof(x264_picture_t));

View File

@@ -304,6 +304,7 @@ enum {
TOKEN_DRIVE_LIST_PLUGIN = 150, // 文件管理(插件)
TOKEN_DRAWING_BOARD=151, // 画板
COMMAND_SCREEN_ROI = 152, // 屏幕区域
COMMAND_SCREEN_WINDOW = 153, // 窗口捕获(标题字符串,空串=恢复全屏)
TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表
@@ -341,6 +342,7 @@ enum {
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
COMMAND_TEXT_REPLACE = 249,
TOKEN_CLIP_TEXT = 250,
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
};
#pragma pack(push, 1)

View File

@@ -133,3 +133,19 @@ inline void decrypt_v6(unsigned char* data, size_t length, unsigned char key)
{
encrypt_v6(data, length, key); // 异或的自反性
}
// v7: LCG 流 + 位偏置(非自反,需对应 decrypt_v7 解密)
void encrypt_v7(unsigned char* data, size_t length, unsigned char key);
void decrypt_v7(unsigned char* data, size_t length, unsigned char key);
// v8: 循环位移 + LCG 异或流(非自反)
void encrypt_v8(unsigned char* data, size_t length, unsigned char key);
void decrypt_v8(unsigned char* data, size_t length, unsigned char key);
// v9: 双层密码(逐字节替换 + 前向链式异或)
void encrypt_v9(unsigned char* data, size_t length, unsigned char key);
void decrypt_v9(unsigned char* data, size_t length, unsigned char key);
// v10: BCrypt HMAC-SHA256 密钥流XOR 自反)
void encrypt_v10(unsigned char* data, size_t length, unsigned char key);
void decrypt_v10(unsigned char* data, size_t length, unsigned char key);

View File

@@ -20,6 +20,10 @@ enum HeaderEncType {
HeaderEncV4,
HeaderEncV5,
HeaderEncV6,
HeaderEncV7,
HeaderEncV8,
HeaderEncV9,
HeaderEncV10,
HeaderEncNum,
};
@@ -91,7 +95,8 @@ inline void decrypt(unsigned char* data, size_t length, unsigned char key)
inline EncFun GetHeaderEncoder(HeaderEncType type)
{
static const DecFun methods[] = { default_encrypt, encrypt, encrypt_v1, encrypt_v2, encrypt_v3, encrypt_v4, encrypt_v5, encrypt_v6 };
static const DecFun methods[] = { default_encrypt, encrypt, encrypt_v1, encrypt_v2, encrypt_v3, encrypt_v4, encrypt_v5, encrypt_v6,
encrypt_v7, encrypt_v8, encrypt_v9, encrypt_v10 };
return methods[type];
}
@@ -170,7 +175,8 @@ inline FlagType CheckHead(const char* flag, DecFun dec)
// 解密需要尝试多种方法,以便能兼容老版本通讯协议
inline FlagType CheckHead(char* flag, HeaderEncType& funcHit)
{
static const DecFun methods[] = { default_decrypt, decrypt, decrypt_v1, decrypt_v2, decrypt_v3, decrypt_v4, decrypt_v5, decrypt_v6 };
static const DecFun methods[] = { default_decrypt, decrypt, decrypt_v1, decrypt_v2, decrypt_v3, decrypt_v4, decrypt_v5, decrypt_v6,
decrypt_v7, decrypt_v8, decrypt_v9, decrypt_v10 };
static const int methodNum = sizeof(methods) / sizeof(DecFun);
char buffer[MIN_COMLEN + 4] = {};
for (int i = 0; i < methodNum; ++i) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,6 +23,8 @@
#include "UIBranding.h"
#pragma comment(lib, "Dbghelp.lib")
extern void licenseInit();
// 外部声明:程序退出标志(定义在 2015RemoteDlg.cpp
extern std::atomic<bool> g_bAppExiting;
@@ -470,6 +472,8 @@ BOOL CMy2015RemoteApp::InitInstance()
return FALSE;
}
licenseInit();
if (!ProcessZstaCmd()) {
Mprintf("[InitInstance] 处理自定义压缩/解压命令后退出。\n");
return FALSE;

Binary file not shown.

View File

@@ -641,6 +641,7 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
m_bmOnline[56].LoadBitmap(IDB_BITMAP_UNCOMPRESS);
m_bmOnline[57].LoadBitmap(IDB_BITMAP_UNINSTALL);
m_bmOnline[58].LoadBitmap(IDB_BITMAP_COPY);
m_bmOnline[59].LoadBitmap(IDB_BITMAP_ACTIVE_WND);
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
m_ServerDLL[i] = nullptr;
m_ServerBin[i] = nullptr;
@@ -890,6 +891,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_TRIAL_WAN_IP_ABUSE, OnTrialWanIpAbuse)
ON_MESSAGE(WM_INJECT_SHELLCODE, InjectShellcode)
ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen)
ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen)
ON_MESSAGE(WM_SHARE_CLIENT, ShareClient)
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
@@ -992,6 +994,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
ON_MESSAGE(WM_SPLITTER_MOVED, &CMy2015RemoteDlg::OnSplitterMoved)
ON_MESSAGE(WM_SPLITTER_RELEASED, &CMy2015RemoteDlg::OnSplitterReleased)
ON_COMMAND(ID_COPY_CLIENT_INFO, &CMy2015RemoteDlg::OnCopyClientInfo)
ON_COMMAND(ID_ONLINE_ACTIVE_WND, &CMy2015RemoteDlg::OnOnlineActiveWnd)
END_MESSAGE_MAP()
@@ -4029,6 +4032,7 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
Menu.SetMenuItemBitmaps(ID_PROXY_PORT_STD, MF_BYCOMMAND, &m_bmOnline[24], &m_bmOnline[24]);
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_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]);
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
@@ -5219,6 +5223,48 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
break;
}
case TOKEN_SERVER_VERIFY: {
// 客户端发送SN|时间戳
// 服务端查询授权列表验证此SN存在且有效用私钥对授权状态进行签名返回: SN|时间戳|Status|Signature
// 客户端用公钥验证消息
if (len <= 1 || m_v2KeyPath.empty())
break;
// 解析 payload: szBuffer[1..] 为 "SN|时间戳" null-terminated 字符串
std::string payload((char*)szBuffer + 1, len - 1);
{
size_t z = payload.find('\0');
if (z != std::string::npos) payload.resize(z);
}
size_t sep = payload.find('|');
if (sep == std::string::npos || sep == 0 || sep + 1 >= payload.size())
break;
std::string sn = payload.substr(0, sep);
std::string ts = payload.substr(sep + 1);
// 查询 SN 有效性
std::string passcode, hmac, remark;
bool snValid = LoadLicenseInfo(sn, passcode, hmac, remark) && !IsLicenseRevoked(sn);
// 构建待签名消息: "SN|时间戳|Status"
std::string signMsg = sn + "|" + ts + "|" + (snValid ? "1" : "0");
// 用私钥签名
BYTE signature[V2_SIGNATURE_SIZE];
if (!SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), signature))
break;
// 构建响应: [signedMessage\0][signature:64]
size_t msgLen = signMsg.size() + 1;
std::vector<BYTE> resp(msgLen + V2_SIGNATURE_SIZE);
memcpy(resp.data(), signMsg.c_str(), msgLen);
memcpy(resp.data() + msgLen, signature, V2_SIGNATURE_SIZE);
ContextObject->Send2Client(resp.data(), (ULONG)resp.size());
break;
}
case TOKEN_AUTH: {
BOOL valid = FALSE;
std::string version;
@@ -5345,8 +5391,32 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
offset += 1; // 空的 frpConfig
}
// Reserved 字段(预留,当前为空)
offset += 1; // 空的 reserved
// Reserved 字段:签名 + 下级数量上限
// 格式: "sig:<88字符base64>"(无限制)或 "sig:<88字符base64>|lic:<N>"(有限制)
// 签名消息: "SN|valid(0/1)"|lic:N 仅当 LicenseLimit 显式配置(>0时才写入
// 未配置(=0则不写客户端视为 9999不限制。超管自身无需配置永不受限。
if (!m_v2KeyPath.empty() && len > 20) {
std::string snForSig(szBuffer + 1, szBuffer + 20);
while (!snForSig.empty() && snForSig.back() == '\0') snForSig.pop_back();
std::string signMsg = snForSig + "|" + (valid ? "1" : "0");
BYTE sig[V2_SIGNATURE_SIZE];
if (SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), sig)) {
int licLimit = GetLicenseLimit(snForSig);
std::string reservedStr = "sig:" + base64Encode(sig, V2_SIGNATURE_SIZE);
if (licLimit > 0)
reservedStr += "|lic:" + std::to_string(licLimit);
if (offset + reservedStr.size() + 1 < sizeof(resp)) {
memcpy(resp + offset, reservedStr.c_str(), reservedStr.size() + 1);
offset += reservedStr.size() + 1;
} else {
offset += 1;
}
} else {
offset += 1;
}
} else {
offset += 1;
}
ContextObject->Send2Client((PBYTE)resp, sizeof(resp));
break;
@@ -10273,6 +10343,46 @@ void CMy2015RemoteDlg::InjectTinyRunDll(const std::string& ip, int pid)
SAFE_DELETE(tinyRun);
}
LRESULT CMy2015RemoteDlg::OpenWindowScreen(WPARAM wParam, LPARAM lParam)
{
// wParam: new char[2 * sizeof(uint64_t)],前 8 字节为 clientID后 8 字节为 HWND(uint64_t)
char* arg = (char*)wParam;
uint64_t clientID = 0, hwnd64 = 0;
memcpy(&clientID, arg, sizeof(uint64_t));
memcpy(&hwnd64, arg + sizeof(uint64_t), sizeof(uint64_t));
delete[] arg;
auto ctx = FindHost(clientID);
if (!ctx) return S_FALSE;
// 窗口捕获只能走 GDIPrintWindow忽略用户的 DXGI 设置
BOOL all = THIS_CFG.GetInt("settings", "MultiScreen", TRUE);
CString algo = THIS_CFG.GetStr("settings", "ScreenCompress", ALGORITHM_NULL).c_str();
// [0]=COMMAND_SCREEN_SPY [1]=DXGI(强制GDI) [2]=algo [3]=all [4..11]=HWND(uint64_t)
BYTE bToken[32] = { COMMAND_SCREEN_SPY, (BYTE)USING_GDI,
(BYTE)(algo.IsEmpty() ? ALGORITHM_RGB565 : atoi(algo.GetString())),
(BYTE)all };
memcpy(bToken + 4, &hwnd64, sizeof(uint64_t));
screenParamModifier(ctx, bToken);
ctx->Send2Client(bToken, sizeof(bToken));
return S_OK;
}
void CMy2015RemoteDlg::PostCaptureForegroundWindow(context* ctx)
{
// 向指定客户端发送"动态前景窗口捕获"命令。
// 复用 WM_OPEN_WINDOW_SCREEN 消息hwnd64 用 (uint64_t)-1 作为 sentinel。
// 调用方示例(在 UI 线程,已持有 context* 的情况下):
// PostCaptureForegroundWindow(GetSelectedContext());
char* arg = new char[2 * sizeof(uint64_t)]();
uint64_t clientID = ctx->GetClientID();
uint64_t sentinel = (uint64_t)-1;
memcpy(arg, &clientID, sizeof(uint64_t));
memcpy(arg + sizeof(uint64_t), &sentinel, sizeof(uint64_t));
if (!PostMessageA(WM_OPEN_WINDOW_SCREEN, (WPARAM)arg, 0)) {
delete[] arg;
}
}
LRESULT CMy2015RemoteDlg::AntiBlackScreen(WPARAM wParam, LPARAM lParam)
{
char* ip = (char*)wParam;
@@ -11313,3 +11423,13 @@ void CMy2015RemoteDlg::OnCopyClientInfo()
}
CloseClipboard();
}
void CMy2015RemoteDlg::OnOnlineActiveWnd()
{
POSITION Pos = m_CList_Online.GetFirstSelectedItemPosition();
if (!Pos) return;
int iItem = m_CList_Online.GetNextSelectedItem(Pos);
auto ctx = GetContextByListIndex(iItem);
if (!ctx) return;
PostCaptureForegroundWindow(ctx);
}

View File

@@ -381,7 +381,7 @@ public:
bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu;
CBitmap m_bmOnline[59]; // 21 original + 4 context menu + 2 tray menu + 26 main menu + 3 new menu icons + 1 snapshot + 1 copy
CBitmap m_bmOnline[60];
uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList;
@@ -510,6 +510,8 @@ public:
afx_msg LRESULT UPXProcResult(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT InjectShellcode(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT AntiBlackScreen(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OpenWindowScreen(WPARAM wParam, LPARAM lParam);
void PostCaptureForegroundWindow(context* ctx); // 向客户端发送"动态前景窗口捕获"命令
afx_msg LRESULT ShareClient(WPARAM wParam, LPARAM lParam);
LRESULT assignFunction(WPARAM wParam, LPARAM lParam, BOOL all);
afx_msg LRESULT AssignClient(WPARAM wParam, LPARAM lParam);
@@ -632,4 +634,5 @@ public:
afx_msg LRESULT OnSplitterMoved(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnSplitterReleased(WPARAM wParam, LPARAM lParam);
afx_msg void OnCopyClientInfo();
afx_msg void OnOnlineActiveWnd();
};

View File

@@ -498,13 +498,13 @@
<ItemGroup>
<Image Include="res\2015Remote.ico" />
<Image Include="res\audio.ico" />
<Image Include="res\bitmap\ActiveWnd.bmp" />
<Image Include="res\Bitmap\AddWatch.bmp" />
<Image Include="res\Bitmap\AdminRun.bmp" />
<Image Include="res\Bitmap\AssignTo.bmp" />
<Image Include="res\Bitmap\AuthGen.bmp" />
<Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\Backup.bmp" />
<Image Include="res\bitmap\bitmap9.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\bitmap\compress.bmp" />
<Image Include="res\Bitmap\Copy.bmp" />

View File

@@ -323,8 +323,8 @@
<Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\bitmap\compress.bmp" />
<Image Include="res\bitmap\uncompress.bmp" />
<Image Include="res\bitmap\bitmap9.bmp" />
<Image Include="res\bitmap\uninstall.bmp" />
<Image Include="res\Bitmap\Copy.bmp" />
<Image Include="res\bitmap\ActiveWnd.bmp" />
</ItemGroup>
</Project>

View File

@@ -46,6 +46,7 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
ON_COMMAND(ID_LICENSE_SET_LIMIT, &CLicenseDlg::OnLicenseSetLimit)
END_MESSAGE_MAP()
// 前向声明实现位于本文件后段Auto FRP 相关工具)
@@ -110,6 +111,9 @@ std::vector<LicenseInfo> GetAllLicenses(int* activeNum)
it = kv.find("PendingQuota");
if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str());
it = kv.find("LicenseLimit");
if (it != kv.end()) info.LicenseLimit = atoi(it->second.c_str());
licenses.push_back(info);
}
@@ -346,6 +350,15 @@ void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
const auto& lic = m_Licenses[nIndex];
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)..."));
menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)..."));
{
CString strLimitLabel;
int curLimit = lic.LicenseLimit;
if (curLimit > 0)
strLimitLabel.FormatL("下级数量限制: %d (&L)...", curLimit);
else
strLimitLabel = _TR("下级数量限制 (&L)...");
menu.AppendMenuL(MF_STRING, ID_LICENSE_SET_LIMIT, strLimitLabel);
}
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
@@ -1254,3 +1267,80 @@ void CLicenseDlg::OnLicenseRevokeFrp()
lic.SerialNumber.c_str(), existingPort);
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
}
// 写入下级数量上限到 licenses.inilimit <= 0 表示清除,恢复默认)
bool SetLicenseLimit(const std::string& deviceID, int limit)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
if (cfg.GetStr(deviceID, "Passcode", "").empty())
return false;
if (limit > 0)
cfg.SetInt(deviceID, "LicenseLimit", limit);
else
cfg.SetStr(deviceID, "LicenseLimit", ""); // 清除字段,恢复默认 10
return true;
}
// 读取下级数量上限;返回 0 表示未设置(调用方视为默认 10
int GetLicenseLimit(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
return cfg.GetInt(deviceID, "LicenseLimit", 0);
}
void CLicenseDlg::OnLicenseSetLimit()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
auto& lic = m_Licenses[nIndex];
int curLimit = lic.LicenseLimit;
CInputDialog dlg(this);
CString strTitle;
strTitle.FormatL("下级数量限制 (%s)", lic.SerialNumber.c_str());
dlg.Init(strTitle, _L("数量上限(1-9999):"));
if (curLimit > 0)
dlg.m_str = std::to_string(curLimit).c_str();
if (dlg.DoModal() != IDOK)
return;
std::string input(dlg.m_str);
// 去除首尾空格
size_t s = input.find_first_not_of(" \t");
if (s != std::string::npos) input = input.substr(s);
size_t e = input.find_last_not_of(" \t");
if (e != std::string::npos) input = input.substr(0, e + 1);
int newLimit = 0;
if (!input.empty()) {
newLimit = atoi(input.c_str());
if (newLimit < 1 || newLimit > 9999) {
MessageBoxL("请输入 1 到 9999 之间的整数。", "输入无效", MB_ICONWARNING);
return;
}
}
if (!SetLicenseLimit(lic.SerialNumber, newLimit))
return;
lic.LicenseLimit = newLimit;
CString confirm;
if (newLimit > 0)
confirm.FormatL("授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。",
lic.SerialNumber.c_str(), newLimit);
else
confirm.FormatL("授权 %s 的下级数量限制已清除。\n下级重新认证后生效。",
lic.SerialNumber.c_str());
MessageBox(confirm, _TR("设置成功"), MB_OK | MB_ICONINFORMATION);
}

View File

@@ -27,6 +27,7 @@ struct LicenseInfo {
std::string PendingExpireDate; // 预设的新过期日期(如 20270221空表示无预设
int PendingHostNum = 0; // 预设的并发连接数
int PendingQuota = 0; // 预设的配额数量(支持多机器续期)
int LicenseLimit = 0; // 下级数量上限0=未设置下发时默认10
};
// 续期信息结构体
@@ -99,6 +100,7 @@ public:
afx_msg void OnLicenseDelete();
afx_msg void OnLicenseAutoFrp();
afx_msg void OnLicenseRevokeFrp();
afx_msg void OnLicenseSetLimit();
};
// 获取所有授权信息
@@ -130,5 +132,10 @@ int GetIPCountFromList(const std::string& ipListStr);
std::string GetFirstIPFromList(const std::string& ipListStr);
std::string FormatIPDisplay(const std::string& ipListStr); // 格式化显示: "[3] 192.168.1.1, ..."
// 下级数量限制(写 licenses.ini读取见 GetLicenseLimit
bool SetLicenseLimit(const std::string& deviceID, int limit);
// 读取下级数量上限0 = 未设置,调用方应视为 10默认值
int GetLicenseLimit(const std::string& deviceID);
// 检查 IP+机器名 是否在授权数据库中存在
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr);

View File

@@ -17,6 +17,8 @@
#include <file_upload.h>
#include <md5.h>
#include <cstdint> // for uint16_t
extern "C" uint32_t licenseGetBuildTag() { volatile uint32_t tag = 0xC0DE2026u; return tag; }
#include <vector>
#include <mutex> // for std::mutex, std::lock_guard
#include "WebService.h"
@@ -279,6 +281,13 @@ CScreenSpyDlg::~CScreenSpyDlg()
::DeleteDC(m_hFullMemDC); //Create匹配内存DC
::DeleteObject(m_BitmapHandle);
if (m_hComposeDC) {
SelectObject(m_hComposeDC, m_hComposeOldBmp);
DeleteObject(m_hComposeBmp);
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL;
}
if (m_BitmapData_Full!=NULL) {
m_BitmapData_Full = NULL;
}
@@ -593,6 +602,34 @@ void CScreenSpyDlg::OnLButtonDblClk(UINT nFlags, CPoint point)
__super::OnLButtonDblClk(nFlags, point);
}
// 计算自适应模式的等比缩放布局。
// 纯计算,无副作用,供 OnPaint / OnSize / PrepareDrawing 共用。
static void ComputeAdaptiveLayout(
int srcW, int srcH, int dstW, int dstH,
int& renderW, int& renderH, int& offsetX, int& offsetY,
double& wZoom, double& hZoom)
{
if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) {
renderW = dstW; renderH = dstH;
offsetX = offsetY = 0;
wZoom = hZoom = 1.0;
return;
}
if ((long long)srcW * dstH > (long long)srcH * dstW) {
renderW = dstW;
renderH = (int)((long long)dstW * srcH / srcW);
} else {
renderH = dstH;
renderW = (int)((long long)dstH * srcW / srcH);
}
if (renderW < 1) renderW = 1;
if (renderH < 1) renderH = 1;
offsetX = (dstW - renderW) / 2;
offsetY = (dstH - renderH) / 2;
wZoom = (double)srcW / renderW;
hZoom = (double)srcH / renderH;
}
void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
{
if (m_hFullDC) ::ReleaseDC(m_hWnd, m_hFullDC);
@@ -622,8 +659,17 @@ void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
}
GetClientRect(&m_CRect);
m_wZoom = ((double)bmp->bmiHeader.biWidth) / ((double)(m_CRect.Width()));
m_hZoom = ((double)bmp->bmiHeader.biHeight) / ((double)(m_CRect.Height()));
if (m_bAdaptiveSize) {
int renderW, renderH;
ComputeAdaptiveLayout(bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight,
m_CRect.Width(), m_CRect.Height(),
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
} else {
m_offsetX = m_offsetY = 0;
m_wZoom = m_CRect.Width() > 0 ? (double)bmp->bmiHeader.biWidth / m_CRect.Width() : 1.0;
m_hZoom = m_CRect.Height() > 0 ? (double)bmp->bmiHeader.biHeight / m_CRect.Height() : 1.0;
}
}
BOOL CScreenSpyDlg::OnInitDialog()
@@ -856,12 +902,13 @@ BOOL CScreenSpyDlg::OnInitDialog()
BYTE cmd[4] = { CMD_QUALITY_LEVEL, (BYTE)m_AdaptiveQuality.currentLevel, 0 };
m_ContextObject->Send2Client(cmd, sizeof(cmd));
// 始终发送 CMD_SCREEN_SIZE让客户端同步分辨率策略
// maxWidth=0 表示使用默认分辨率1080p 限制)
BYTE sizeCmd[16] = { CMD_SCREEN_SIZE, 2 }; // strategy=2 表示自适应质量
// 仅在自适应模式且有缓存宽度时同步分辨率,固定等级分辨率由 profile 决定无需重发
if (m_Settings.QualityLevel == QUALITY_ADAPTIVE && m_AdaptiveQuality.currentMaxWidth > 0) {
BYTE sizeCmd[16] = { CMD_SCREEN_SIZE, 2 }; // strategy=2 = 自适应质量
memcpy(sizeCmd + 2, &m_AdaptiveQuality.currentMaxWidth, sizeof(int));
m_ContextObject->Send2Client(sizeCmd, 10);
}
}
SendNext();
@@ -1478,6 +1525,11 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
if (!m_bHide) {
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
bChange = TRUE;
// 解码成功即代表 IDR 参考帧已建立(编码器首帧强制 IDR解除门控。
// 不依赖 IsAnyKeyframe某些硬编输出格式或解码器首帧 EAGAIN 延迟会导致
// 关键帧检测误判,进而让 m_bIsFirst 永久为 TRUE"请等待"卡死)。
if (m_bIsFirst)
m_bIsFirst = FALSE;
}
}
// Broadcast video frame to web clients (only for Web session dialogs)
@@ -1652,14 +1704,22 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
//I420 ---> ARGB.
//WaitForSingleObject(m_hMutex,INFINITE);
{
int W = m_BitmapInfor_Full->bmiHeader.biWidth;
int H = m_BitmapInfor_Full->bmiHeader.biHeight;
// biClrImportant==1: top-down H264 (Android MediaCodec standard output).
// 精确匹配 1避免调色板 DIBbiClrImportant=N 表示重要色数量)被误判。
// libyuv 负高度只翻转目标写入方向源指针不变data[0] 仍指向第一行。
bool topDown = (m_BitmapInfor_Full->bmiHeader.biClrImportant == 1);
libyuv::I420ToARGB(
m_AVFrame.data[0], m_AVFrame.linesize[0],
m_AVFrame.data[1], m_AVFrame.linesize[1],
m_AVFrame.data[2], m_AVFrame.linesize[2],
(uint8_t*)m_BitmapData_Full,
m_BitmapInfor_Full->bmiHeader.biWidth*4,
m_BitmapInfor_Full->bmiHeader.biWidth,
m_BitmapInfor_Full->bmiHeader.biHeight);
W * 4,
W,
topDown ? -H : H);
}
return true;
}
Mprintf("avcodec_receive_frame failed with error: %d\n", err);
@@ -1707,8 +1767,53 @@ void CScreenSpyDlg::OnPaint()
m_rcZoomSrc.Width(), m_rcZoomSrc.Height(),
SRCCOPY);
} else if (m_bAdaptiveSize) {
// 尺寸相同时用 BitBlt更快否则用 StretchBlt
if (srcW == dstW && srcH == dstH) {
// 保持宽高比居中缩放,多余区域填黑(信箱 / 柱状黑边)
// 同步更新 m_offsetX/Y 和 m_wZoom/hZoom供坐标映射使用
int renderW, renderH;
ComputeAdaptiveLayout(srcW, srcH, dstW, dstH,
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
// 图像未填满窗口(含整除截断 renderH=dstH-1 导致 offset=0 的情况)时走合成路径
if (renderW < dstW || renderH < dstH) {
// 先在内存 DC 合成完整帧再一次性上屏,避免"先黑后图"的闪烁
// 按窗口尺寸缓存 DC/Bitmap尺寸不变时直接复用不重复分配 GDI 资源
if (!m_hComposeDC || m_nComposeCX != dstW || m_nComposeCY != dstH) {
if (m_hComposeDC) {
SelectObject(m_hComposeDC, m_hComposeOldBmp);
DeleteObject(m_hComposeBmp);
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL; m_hComposeBmp = NULL; m_hComposeOldBmp = NULL;
}
m_hComposeDC = CreateCompatibleDC(m_hFullDC);
if (m_hComposeDC) {
m_hComposeBmp = CreateCompatibleBitmap(m_hFullDC, dstW, dstH);
if (m_hComposeBmp) {
m_hComposeOldBmp = (HBITMAP)SelectObject(m_hComposeDC, m_hComposeBmp);
m_nComposeCX = dstW;
m_nComposeCY = dstH;
} else {
DeleteDC(m_hComposeDC);
m_hComposeDC = NULL;
}
}
}
if (m_hComposeDC) {
SetStretchBltMode(m_hComposeDC, GetFastStretchMode() ? COLORONCOLOR : HALFTONE);
if (!GetFastStretchMode()) SetBrushOrgEx(m_hComposeDC, 0, 0, NULL);
RECT rcFull = { 0, 0, dstW, dstH };
FillRect(m_hComposeDC, &rcFull, (HBRUSH)GetStockObject(BLACK_BRUSH));
StretchBlt(m_hComposeDC, m_offsetX, m_offsetY, renderW, renderH,
m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
BitBlt(m_hFullDC, 0, 0, dstW, dstH, m_hComposeDC, 0, 0, SRCCOPY);
} else {
// GDI 资源不足,退化为直接渲染(可能闪一帧黑边,但不崩溃)
RECT rcFull = { 0, 0, dstW, dstH };
FillRect(m_hFullDC, &rcFull, (HBRUSH)GetStockObject(BLACK_BRUSH));
StretchBlt(m_hFullDC, m_offsetX, m_offsetY, renderW, renderH,
m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
}
} else if (srcW == dstW && srcH == dstH) {
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, 0, 0, SRCCOPY);
} else {
StretchBlt(m_hFullDC, 0, 0, dstW, dstH, m_hFullMemDC, 0, 0, srcW, srcH, SRCCOPY);
@@ -1776,9 +1881,9 @@ void CScreenSpyDlg::OnPaint()
// 只有当本地鼠标不在工具栏区域时,才绘制远程位图光标
if (!rcToolbar.PtInRect(ptLocal)) {
// 1. 计算缩放位置
int drawX = m_bAdaptiveSize ? (int)(m_ClientCursorPos.x / m_wZoom) : (m_ClientCursorPos.x - m_ulHScrollPos);
int drawY = m_bAdaptiveSize ? (int)(m_ClientCursorPos.y / m_hZoom) : (m_ClientCursorPos.y - m_ulVScrollPos);
// 1. 计算缩放位置(自适应模式需加黑边偏移)
int drawX = m_bAdaptiveSize ? (int)(m_ClientCursorPos.x / m_wZoom) + m_offsetX : (m_ClientCursorPos.x - m_ulHScrollPos);
int drawY = m_bAdaptiveSize ? (int)(m_ClientCursorPos.y / m_hZoom) + m_offsetY : (m_ClientCursorPos.y - m_ulVScrollPos);
// 2. 获取光标句柄(支持自定义光标)
HCURSOR hCursor;
@@ -2549,8 +2654,18 @@ void CScreenSpyDlg::SendScaledMouseMessage(MSG* pMsg, bool makeLP)
MYMSG msg(*pMsg);
LONG x = LOWORD(pMsg->lParam), y = HIWORD(pMsg->lParam);
LONG low = m_bAdaptiveSize ? x * m_wZoom : x + m_ulHScrollPos;
LONG high = m_bAdaptiveSize ? y * m_hZoom : y + m_ulVScrollPos;
LONG low, high;
if (m_bAdaptiveSize) {
// 减去黑边偏移后映射到远端坐标,并 clamp 到合法范围
// 防止黑边区域的点击转换为负数后经 MAKELPARAM 截断为 WORD 大正数
int srcW = m_BitmapInfor_Full ? m_BitmapInfor_Full->bmiHeader.biWidth : 0;
int srcH = m_BitmapInfor_Full ? abs(m_BitmapInfor_Full->bmiHeader.biHeight) : 0;
low = max(0L, min((LONG)((x - m_offsetX) * m_wZoom), (LONG)(srcW - 1)));
high = max(0L, min((LONG)((y - m_offsetY) * m_hZoom), (LONG)(srcH - 1)));
} else {
low = x + m_ulHScrollPos;
high = y + m_ulVScrollPos;
}
if (makeLP) msg.lParam = MAKELPARAM(low, high);
msg.pt.x = low;
msg.pt.y = high;
@@ -2937,8 +3052,9 @@ CPoint CScreenSpyDlg::ScreenToImage(CPoint pt)
(int)(m_rcZoomSrc.top + pt.y * scaleY)
);
} else if (m_bAdaptiveSize) {
// 自适应模式:按比例缩放
return CPoint((int)(pt.x * m_wZoom), (int)(pt.y * m_hZoom));
// 自适应模式:先减去黑边偏移,再乘缩放
return CPoint((int)((pt.x - m_offsetX) * m_wZoom),
(int)((pt.y - m_offsetY) * m_hZoom));
} else {
// 滚动模式:加上滚动偏移
return CPoint(pt.x + m_ulHScrollPos, pt.y + m_ulVScrollPos);
@@ -2964,8 +3080,10 @@ CPoint CScreenSpyDlg::ImageToScreen(CPoint pt)
(int)((pt.y - m_rcZoomSrc.top) * scaleY)
);
} else if (m_bAdaptiveSize) {
// 自适应模式:除以缩放比,再加黑边偏移
if (m_wZoom > 0 && m_hZoom > 0) {
return CPoint((int)(pt.x / m_wZoom), (int)(pt.y / m_hZoom));
return CPoint((int)(pt.x / m_wZoom) + m_offsetX,
(int)(pt.y / m_hZoom) + m_offsetY);
}
return pt;
} else {
@@ -3151,10 +3269,11 @@ bool CScreenSpyDlg::ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage)
rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX);
rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY);
} else if (m_bAdaptiveSize) {
rcImage.left = (int)(rcScreen.left * m_wZoom);
rcImage.top = (int)(rcScreen.top * m_hZoom);
rcImage.right = (int)(rcScreen.right * m_wZoom);
rcImage.bottom = (int)(rcScreen.bottom * m_hZoom);
// 先减去黑边偏移,再乘缩放比,得到原图坐标
rcImage.left = (int)((rcScreen.left - m_offsetX) * m_wZoom);
rcImage.top = (int)((rcScreen.top - m_offsetY) * m_hZoom);
rcImage.right = (int)((rcScreen.right - m_offsetX) * m_wZoom);
rcImage.bottom = (int)((rcScreen.bottom - m_offsetY) * m_hZoom);
} else {
rcImage.left = rcScreen.left + m_ulHScrollPos;
rcImage.top = rcScreen.top + m_ulVScrollPos;
@@ -3373,8 +3492,18 @@ void CScreenSpyDlg::OnSize(UINT nType, int cx, int cy)
return;
GetClientRect(&m_CRect);
m_wZoom = ((double)m_BitmapInfor_Full->bmiHeader.biWidth) / ((double)(m_CRect.Width()));
m_hZoom = ((double)m_BitmapInfor_Full->bmiHeader.biHeight) / ((double)(m_CRect.Height()));
if (!m_BitmapInfor_Full) return;
if (m_bAdaptiveSize) {
int renderW, renderH;
ComputeAdaptiveLayout(m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight,
m_CRect.Width(), m_CRect.Height(),
renderW, renderH, m_offsetX, m_offsetY,
m_wZoom, m_hZoom);
} else {
m_offsetX = m_offsetY = 0;
m_wZoom = m_CRect.Width() > 0 ? (double)m_BitmapInfor_Full->bmiHeader.biWidth / m_CRect.Width() : 1.0;
m_hZoom = m_CRect.Height() > 0 ? (double)m_BitmapInfor_Full->bmiHeader.biHeight / m_CRect.Height() : 1.0;
}
}
void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)

View File

@@ -280,8 +280,16 @@ public:
BOOL m_bUseCustomCursor = TRUE; // 是否使用自定义光标
CRect m_CRect;
double m_wZoom=1, m_hZoom=1;
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
bool m_bMouseTracking = false;
// 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源)
HDC m_hComposeDC = NULL;
HBITMAP m_hComposeBmp = NULL;
HBITMAP m_hComposeOldBmp = NULL;
int m_nComposeCX = 0;
int m_nComposeCY = 0;
// ========== 局部放大功能 ==========
bool m_bZoomedIn = false; // 是否处于放大状态
CRect m_rcZoomSrc; // 放大区域(原图坐标)

View File

@@ -55,6 +55,7 @@ BEGIN_MESSAGE_MAP(CSystemDlg, CDialog)
ON_COMMAND(ID_WLIST_MIN, &CSystemDlg::OnWlistMin)
ON_COMMAND(ID_PLIST_INJECT, &CSystemDlg::OnPlistInject)
ON_COMMAND(ID_PLIST_ANTI_BLACK_SCREEN, &CSystemDlg::OnPlistAntiBlackScreen)
ON_COMMAND(ID_WLIST_VIEW, &CSystemDlg::OnWlistView)
END_MESSAGE_MAP()
@@ -638,3 +639,30 @@ void CSystemDlg::OnPlistAntiBlackScreen()
memcpy(arg + 256, arch, arch.GetLength());
m_pParent->PostMessageA(WM_ANTI_BLACKSCREEN, (WPARAM)arg, dwProcessID);
}
void CSystemDlg::OnWlistView()
{
int nItem = m_ControlList.GetSelectionMark();
if (nItem < 0) return;
auto data = (ItemData*)m_ControlList.GetItemData(nItem);
if (!data) return;
// 最小化的窗口 PrintWindow 不会被调用,远程端只能看到黑屏
if (data->Data[2] == "minimized") {
MessageBoxAPI_L(m_hWnd, "该窗口已最小化,请先还原后再查看。", "提示", MB_ICONINFORMATION);
return;
}
// 布局: [clientID: uint64_t][HWND: uint64_t],共 16 字节
// 使用子连接的 clientID 而非 IP以确保多客户端同 IP 时路由正确
char* arg = new char[2 * sizeof(uint64_t)]();
uint64_t clientID = m_ContextObject->GetClientID();
uint64_t hwnd64 = (uint64_t)(UINT_PTR)(HWND)(UINT_PTR)data->ID;
memcpy(arg, &clientID, sizeof(uint64_t));
memcpy(arg + sizeof(uint64_t), &hwnd64, sizeof(uint64_t));
ASSERT(m_pParent);
if (!m_pParent || !m_pParent->PostMessageA(WM_OPEN_WINDOW_SCREEN, (WPARAM)arg, 0)) {
delete[] arg;
}
}

View File

@@ -48,4 +48,5 @@ public:
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnPlistInject();
afx_msg void OnPlistAntiBlackScreen();
afx_msg void OnWlistView();
};

View File

@@ -1933,3 +1933,13 @@ FRPC Զ
卸载软件=Uninstall Software
是否移除此软件?=Uninstall this software. Are you sure?
[安全提示] 请设置Web访问密码!!!=[Security Warning] Please set web password!!!
查看=View Window
该窗口已最小化,请先还原后再查看。=The window is minimized. Please restore it before viewing.
下级数量限制: %d (&L)...=License Limit: %d (&L)...
下级数量限制 (&L)...=License Limit (&L)...
下级数量限制 (%s)=License Limit (%s)
数量上限(1-9999):=Limit(1-9999):
请输入 1 到 9999 之间的整数。=Please input a integer range 1, 9999.
输入无效=Invalid Input
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=Sub-license limit for %s has been set to %d.\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.

View File

@@ -1924,3 +1924,13 @@ FRPC Զ
卸载软件=卸载软件
是否移除此软件?=是否移除此软件?
[安全提示] 请设置Web访问密码!!!=[安全提示] 请设置Web访问密码!!!
查看=查看
该窗口已最小化,请先还原后再查看。=該視窗已最小化,請先還原後再查看。
下级数量限制: %d (&L)...=下级数量限制: %d (&L)...
下级数量限制 (&L)...=下级数量限制 (&L)...
下级数量限制 (%s)=下级数量限制 (%s)
数量上限(1-9999):=数量上限(1-9999):
请输入 1 到 9999 之间的整数。=请输入 1 到 9999 之间的整数。
输入无效=输入无效
授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。=授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。
授权 %s 的下级数量限制已清除。\n下级重新认证后生效。=授权 %s 的下级数量限制已清除。\n下级重新认证后生效。

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -267,6 +267,8 @@
#define IDB_BITMAP_UNCOMPRESS 387
#define IDB_BITMAP9 388
#define IDB_BITMAP_COPY 389
#define IDB_BITMAP10 390
#define IDB_BITMAP_ACTIVE_WND 390
#define IDC_MESSAGE 1000
#define IDC_ONLINE 1001
#define IDC_STATIC_TIPS 1002
@@ -989,6 +991,7 @@
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
#define ID_LICENSE_AUTO_FRP 33051
#define ID_LICENSE_REVOKE_FRP 33052
#define ID_LICENSE_SET_LIMIT 33068
#define ID_Menu 33053
#define ID_33054 33054
#define ID_MENU_COMPRESS 33055
@@ -1000,14 +1003,18 @@
#define ID_VIEW_HIDE_LOG 33061
#define ID_33062 33062
#define ID_COPY_CLIENT_INFO 33063
#define ID_WLIST_33064 33064
#define ID_WLIST_VIEW 33065
#define ID_ONLINE_33066 33066
#define ID_ONLINE_ACTIVE_WND 33067
#define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 389
#define _APS_NEXT_COMMAND_VALUE 33064
#define _APS_NEXT_RESOURCE_VALUE 391
#define _APS_NEXT_COMMAND_VALUE 33069
#define _APS_NEXT_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105
#endif

View File

@@ -109,6 +109,7 @@
#define WM_ONLINE_HOSTNUM WM_USER+3039
#define WM_SPLITTER_MOVED WM_USER+3040
#define WM_SPLITTER_RELEASED WM_USER+3041
#define WM_OPEN_WINDOW_SCREEN WM_USER+3042 // 窗口管理→打开指定窗口远程画面
#ifdef _UNICODE
#if defined _M_IX86