Compare commits
6 Commits
0aa75882d1
...
566f5b8d42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566f5b8d42 | ||
|
|
70a6b0128e | ||
|
|
b252cbbaf2 | ||
|
|
5f4fb62d20 | ||
|
|
ef8165c3b4 | ||
|
|
2c5b5ad628 |
@@ -199,6 +199,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -241,6 +242,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<ClCompile Include="RegisterOperation.cpp" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ShellManager.cpp" />
|
||||
@@ -70,6 +71,7 @@
|
||||
<ClInclude Include="ScreenCapture.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ShellManager.h" />
|
||||
|
||||
@@ -1163,6 +1163,7 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
|
||||
|
||||
// 创建新连接发送文件
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(allFiles, targetDir, pClient,
|
||||
|
||||
@@ -100,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
|
||||
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
|
||||
}
|
||||
|
||||
// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。
|
||||
// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款),
|
||||
// 空 publicKey/privateKey 走内置 HMAC。
|
||||
extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
|
||||
bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs)
|
||||
{
|
||||
ConnAuthPacket pkt = {};
|
||||
pkt.token = TOKEN_CONN_AUTH;
|
||||
pkt.clientID = clientID;
|
||||
pkt.timestamp = (uint64_t)time(NULL);
|
||||
// 16 字节 nonce:用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳)
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF);
|
||||
}
|
||||
|
||||
BYTE sigInput[8 + 8 + 16];
|
||||
memcpy(sigInput, &pkt.clientID, 8);
|
||||
memcpy(sigInput + 8, &pkt.timestamp, 8);
|
||||
memcpy(sigInput + 16, pkt.nonce, 16);
|
||||
auto sig = signMessage("", sigInput, sizeof(sigInput));
|
||||
size_t sigLen = sig.size() < 64 ? sig.size() : 64;
|
||||
memcpy(pkt.signature, sig.data(), sigLen);
|
||||
|
||||
// 设置等待状态
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authStatus = -1;
|
||||
m_authPending = true;
|
||||
}
|
||||
|
||||
// 发包;用 HttpMask 包装与其它子连接首包风格一致
|
||||
HttpMask mask(DEFAULT_HOST, GetClientIPHeader());
|
||||
int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask);
|
||||
if (sent <= 0) {
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
m_authPending = false;
|
||||
Mprintf("[ConnAuth] 发送失败\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 等响应或超时
|
||||
std::unique_lock<std::mutex> lk(m_authMtx);
|
||||
bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
|
||||
[this]{ return !m_authPending; });
|
||||
int status = m_authStatus;
|
||||
m_authPending = false;
|
||||
if (!got) {
|
||||
Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs);
|
||||
return false;
|
||||
}
|
||||
bool ok = (status == CONN_AUTH_OK);
|
||||
Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
|
||||
{
|
||||
if (!buf || len < sizeof(ConnAuthAck)) return false;
|
||||
if (buf[0] != TOKEN_CONN_AUTH) return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_authMtx);
|
||||
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
|
||||
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
|
||||
m_authStatus = ack->status;
|
||||
m_authPending = false;
|
||||
}
|
||||
m_authCv.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
|
||||
const std::string& pubIP, void* main) : g_bExit(bExit)
|
||||
@@ -119,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
|
||||
}
|
||||
|
||||
m_main = main;
|
||||
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
|
||||
// m_conn->clientID 现取主连接 ID(同一指针,主连接登录后填好的最新值)。
|
||||
int encoder = conn ? conn->GetHeaderEncType() : 0;
|
||||
m_sLocPublicIP = pubIP;
|
||||
m_ServerAddr = {};
|
||||
@@ -380,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
|
||||
#endif
|
||||
}
|
||||
|
||||
// 子连接身份校验(opt-in 通过 EnableSubConnAuth 开启):
|
||||
// - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。
|
||||
// - clientID 优先用 EnableSubConnAuth 显式传入的值(Linux/macOS 客户端走此路径),
|
||||
// 未显式传入时从 m_conn 现取(Windows 客户端走此路径)。
|
||||
// - 校验失败:Disconnect 并返回 FALSE,让上层走重连或放弃逻辑。
|
||||
if (m_subConnAuthEnabled) {
|
||||
uint64_t cid = m_subConnAuthClientID;
|
||||
if (cid == 0 && m_conn) cid = m_conn->clientID;
|
||||
if (cid == 0) {
|
||||
Mprintf("[ConnAuth] 跳过校验:clientID 尚未就绪(主连接还没拿到 ID)\n");
|
||||
// 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) {
|
||||
Mprintf("[ConnAuth] 校验失败,断开连接\n");
|
||||
Disconnect();
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -549,11 +643,16 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
|
||||
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
|
||||
|
||||
if (Z_SUCCESS(iRet)) { //如果解压成功
|
||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||
if (ret) {
|
||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
|
||||
// 不在等待状态时返回 false,包透传给 manager(manager 一般也不识别此 token,
|
||||
// 走 default 路径忽略,无副作用)。
|
||||
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
|
||||
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
|
||||
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
|
||||
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
|
||||
if (ret) {
|
||||
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
#endif
|
||||
#include "IOCPBase.h"
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
|
||||
#define MAX_RECV_BUFFER 1024*32
|
||||
#define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率
|
||||
@@ -259,6 +261,26 @@ public:
|
||||
m_LoginMsg = msg;
|
||||
m_LoginSignature = hmac;
|
||||
}
|
||||
|
||||
// 子连接身份校验:发 TOKEN_CONN_AUTH 包,等服务端 ConnAuthAck 响应。
|
||||
// 返回 true 表示通过,false 表示超时/失败/网络错误。
|
||||
// 主连接不调用此方法。新客户端必须调用并校验成功后才能继续后续命令。
|
||||
// 已实现的协议扩展(如 KeyBoard 子连接的 cap word)保留不变,与本机制并行工作。
|
||||
bool PerformConnAuth(uint64_t clientID, int timeoutMs);
|
||||
|
||||
// 让 ConnectServer 在每次成功后自动调一次 PerformConnAuth(opt-in)。
|
||||
// 子连接构造后调用此方法启用。
|
||||
// - clientID == 0:每次 auth 时从 m_conn->clientID 现取(Windows 客户端走此路径)。
|
||||
// 这样即便 IOCPClient 创建时主连接还没拿到 ID,真正连上时也能用到最新值。
|
||||
// - clientID != 0:显式指定(Linux/macOS 客户端 IOCPClient 不带 m_conn 时用此参数)。
|
||||
void EnableSubConnAuth(bool enabled = true, uint64_t clientID = 0) {
|
||||
m_subConnAuthEnabled = enabled;
|
||||
m_subConnAuthClientID = clientID;
|
||||
}
|
||||
|
||||
// 内部:在收到的数据帧分发到 manager 之前,尝试识别并消费 TOKEN_CONN_AUTH ack。
|
||||
// 仅在我们正在等待 auth 响应时(m_authPending=true)才消费;否则透传给 manager。
|
||||
bool TryHandleAuthResponse(PBYTE buf, ULONG len);
|
||||
protected:
|
||||
virtual int ReceiveData(char* buffer, int bufSize, int flags)
|
||||
{
|
||||
@@ -285,6 +307,16 @@ protected:
|
||||
BOOL m_bConnected;
|
||||
|
||||
std::mutex m_Locker;
|
||||
|
||||
// 子连接身份校验同步状态。仅在 PerformConnAuth 调用期间生效。
|
||||
std::mutex m_authMtx;
|
||||
std::condition_variable m_authCv;
|
||||
int m_authStatus = -1; // -1 = 未启动;其它 = ConnAuthStatus
|
||||
bool m_authPending = false; // true 时 TryHandleAuthResponse 才消费 ack
|
||||
|
||||
// ConnectServer 成功后自动 auth 的 opt-in 标志。子连接构造后调 EnableSubConnAuth() 设为 true。
|
||||
bool m_subConnAuthEnabled = false;
|
||||
uint64_t m_subConnAuthClientID = 0; // 0 表示从 m_conn->clientID 现取
|
||||
#if USING_CTX
|
||||
ZSTD_CCtx* m_Cctx; // 压缩上下文
|
||||
ZSTD_DCtx* m_Dctx; // 解压上下文
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "auto_start.h"
|
||||
#include "ShellcodeInj.h"
|
||||
#include "KeyboardManager.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "common/file_upload.h"
|
||||
#include "common/DateVerify.h"
|
||||
#include "common/LANChecker.h"
|
||||
@@ -53,7 +54,9 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub
|
||||
{
|
||||
ThreadInfo *tKeyboard = new ThreadInfo();
|
||||
tKeyboard->run = FOREVER_RUN;
|
||||
tKeyboard->p = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
tKeyboard->p = sub;
|
||||
tKeyboard->conn = conn;
|
||||
tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL);
|
||||
return tKeyboard;
|
||||
@@ -956,7 +959,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
}
|
||||
|
||||
case COMMAND_PROXY: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1060,33 +1067,49 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (m_hKeyboard) {
|
||||
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
|
||||
} else {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_TALK: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].user = m_hInstance;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SHELL: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SYSTEM: { //远程进程管理
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_WSLIST: { //远程窗口管理
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1115,20 +1138,65 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_PREVIEW_REQ: {
|
||||
if (ulLength < sizeof(ScreenPreviewReq)) break;
|
||||
ScreenPreviewReq req;
|
||||
memcpy(&req, szBuffer, sizeof(req));
|
||||
// 限流:同一时刻最多 1 个抓屏任务在跑,防御服务端洪泛或异常重发把客户端打爆
|
||||
static std::atomic<int> s_inFlight{0};
|
||||
if (s_inFlight.fetch_add(1) >= 1) {
|
||||
s_inFlight.fetch_sub(1);
|
||||
break; // 直接丢弃,让服务端 4s 超时降级为"预览不可用"
|
||||
}
|
||||
// 投递到工作线程,避免阻塞 OnReceive;抓屏 + 编码可能耗几十毫秒
|
||||
std::thread([this, req]() {
|
||||
struct Guard { ~Guard(){ s_inFlight.fetch_sub(1); } } guard;
|
||||
std::vector<unsigned char> jpg;
|
||||
int w = 0, h = 0;
|
||||
int st = CaptureAndEncodePreview(req.maxWidth, req.jpegQuality, jpg, w, h);
|
||||
|
||||
std::vector<BYTE> pkt(sizeof(ScreenPreviewRspHeader) + (st == SCREEN_PREVIEW_OK ? jpg.size() : 0));
|
||||
ScreenPreviewRspHeader* hdr = reinterpret_cast<ScreenPreviewRspHeader*>(pkt.data());
|
||||
memset(hdr, 0, sizeof(*hdr));
|
||||
hdr->token = TOKEN_SCREEN_PREVIEW_RSP;
|
||||
hdr->reqId = req.reqId;
|
||||
hdr->status = (uint8_t)st;
|
||||
hdr->format = SCREEN_PREVIEW_FMT_JPEG;
|
||||
hdr->width = (uint16_t)w;
|
||||
hdr->height = (uint16_t)h;
|
||||
hdr->bytes = (uint32_t)(st == SCREEN_PREVIEW_OK ? jpg.size() : 0);
|
||||
if (st == SCREEN_PREVIEW_OK && !jpg.empty()) {
|
||||
memcpy(pkt.data() + sizeof(*hdr), jpg.data(), jpg.size());
|
||||
}
|
||||
if (m_ClientObject && m_ClientObject->IsConnected()) {
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (int)pkt.size());
|
||||
}
|
||||
}).detach();
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SCREEN_SPY: {
|
||||
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
|
||||
if (ulLength > 1) {
|
||||
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
|
||||
if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount].user = user;
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_LIST_DRIVE : {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
@@ -1136,25 +1204,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_WEBCAM: {
|
||||
static bool hasCamera = WebCamIsExist();
|
||||
if (!hasCamera) break;
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_AUDIO: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_REGEDIT: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);;
|
||||
break;
|
||||
}
|
||||
|
||||
case COMMAND_SERVICES: {
|
||||
m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
{
|
||||
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
|
||||
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
m_hThread[m_ulThreadCount].p = sub;
|
||||
}
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
@@ -1272,6 +1356,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
opts.enableResume = queryPending; // 只有发送了查询才等待响应
|
||||
|
||||
IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn);
|
||||
pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
|
||||
if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) {
|
||||
std::thread([files, targetDir, pClient, opts, hash, hmac]() {
|
||||
FileBatchTransferWorkerV2(files, targetDir, pClient,
|
||||
|
||||
@@ -225,7 +225,10 @@ std::string GetCurrentUserNameA()
|
||||
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
// 基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
|
||||
// 老算法:基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH }
|
||||
// 注意:pubIP 不稳定(DHCP/换网络)会让 ID 跳变;同 hostname+同安装路径的多机会撞库。
|
||||
// 保留此函数仅为协议兼容(老服务端仍按这个算法验算 RES_CLIENT_ID)。
|
||||
uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
{
|
||||
std::string s;
|
||||
@@ -236,6 +239,52 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
// 读取 Windows 安装时生成的机器 GUID。
|
||||
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
|
||||
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
|
||||
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
|
||||
static std::string GetMachineGuidWindows()
|
||||
{
|
||||
HKEY hKey = NULL;
|
||||
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
|
||||
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Cryptography",
|
||||
0, KEY_READ | KEY_WOW64_64KEY, &hKey) != ERROR_SUCCESS) {
|
||||
return std::string();
|
||||
}
|
||||
char buf[64] = {};
|
||||
DWORD sz = sizeof(buf) - 1; // 留 1 字节给 NUL
|
||||
DWORD type = 0;
|
||||
LSTATUS s = RegQueryValueExA(hKey, "MachineGuid", NULL, &type,
|
||||
(BYTE*)buf, &sz);
|
||||
RegCloseKey(hKey);
|
||||
if (s != ERROR_SUCCESS || type != REG_SZ) return std::string();
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// 路径归一化:先尝试展开成长路径(如 PROGRA~1 -> Program Files),再小写化。
|
||||
// 用于 V2 ID 的输入,保证大小写或长短名变化时同一可执行文件得到同一 ID。
|
||||
static std::string NormalizeExePathLower(const char* path)
|
||||
{
|
||||
char longPath[MAX_PATH] = {};
|
||||
if (GetLongPathNameA(path, longPath, MAX_PATH) == 0) {
|
||||
// 展开失败(路径不存在等罕见情况):直接用原值
|
||||
strcpy_s(longPath, path);
|
||||
}
|
||||
CharLowerA(longPath); // 原地小写化(对 ASCII 简单,对中文路径会按宽字符规则处理)
|
||||
return std::string(longPath);
|
||||
}
|
||||
|
||||
// 新算法:machineGuid + 归一化路径
|
||||
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
|
||||
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
|
||||
// - 一台机两份程序在不同目录 → ID 不同。
|
||||
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath)
|
||||
{
|
||||
std::string s = machineGuid + "|" + normalizedPath;
|
||||
return XXH64(s.c_str(), s.length(), 0);
|
||||
}
|
||||
|
||||
BOOL IsAuthKernel(std::string &str) {
|
||||
BOOL isAuthKernel = FALSE;
|
||||
std::string pid = std::to_string(GetCurrentProcessId());
|
||||
@@ -332,7 +381,17 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
|
||||
LoginInfor.AddReserved(IsRunningAsAdmin());
|
||||
char cpuInfo[32];
|
||||
sprintf(cpuInfo, "%dMHz", dwCPUMHz);
|
||||
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||
// V2 ID 算法:MachineGuid + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移)
|
||||
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库
|
||||
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。
|
||||
std::string machineGuid = GetMachineGuidWindows();
|
||||
if (!machineGuid.empty()) {
|
||||
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf));
|
||||
} else {
|
||||
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
|
||||
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
|
||||
}
|
||||
auto clientID = std::to_string(conn.clientID);
|
||||
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
|
||||
char reservedInfo[64];
|
||||
|
||||
188
client/ScreenPreview.cpp
Normal file
188
client/ScreenPreview.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
// ScreenPreview.cpp
|
||||
#include "stdafx.h"
|
||||
#include "ScreenPreview.h"
|
||||
#include "../common/commands.h" // ScreenPreviewStatus
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
|
||||
// GDI+ 进程级初始化(与 Bmp2Video 互不冲突;Startup 可重入计数)
|
||||
struct GdiplusBoot {
|
||||
ULONG_PTR token = 0;
|
||||
bool ok = false;
|
||||
GdiplusBoot()
|
||||
{
|
||||
GdiplusStartupInput in;
|
||||
ok = (GdiplusStartup(&token, &in, NULL) == Ok);
|
||||
}
|
||||
~GdiplusBoot()
|
||||
{
|
||||
if (ok) GdiplusShutdown(token);
|
||||
}
|
||||
};
|
||||
static GdiplusBoot g_boot;
|
||||
|
||||
int GetJpegEncoderClsid(CLSID& clsid)
|
||||
{
|
||||
UINT num = 0, size = 0;
|
||||
GetImageEncodersSize(&num, &size);
|
||||
if (size == 0) return -1;
|
||||
|
||||
std::vector<BYTE> buf(size);
|
||||
ImageCodecInfo* info = reinterpret_cast<ImageCodecInfo*>(buf.data());
|
||||
GetImageEncoders(num, size, info);
|
||||
for (UINT i = 0; i < num; ++i) {
|
||||
if (wcscmp(info[i].MimeType, L"image/jpeg") == 0) {
|
||||
clsid = info[i].Clsid;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 抓主屏到 24bpp Bitmap,目标尺寸已等比换算。
|
||||
// 返回新分配的 Bitmap*,失败返回 nullptr。调用者负责 delete。
|
||||
Bitmap* GrabPrimaryScaled(int targetW, int targetH)
|
||||
{
|
||||
HDC hScreen = GetDC(NULL);
|
||||
if (!hScreen) return nullptr;
|
||||
|
||||
int srcX = GetSystemMetrics(SM_XVIRTUALSCREEN); // 主屏左上 — 仅取主屏时用 0,0
|
||||
int srcY = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
(void)srcX; (void)srcY;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) {
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HDC hMem = CreateCompatibleDC(hScreen);
|
||||
HBITMAP hBmp = CreateCompatibleBitmap(hScreen, targetW, targetH);
|
||||
if (!hMem || !hBmp) {
|
||||
if (hBmp) DeleteObject(hBmp);
|
||||
if (hMem) DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HGDIOBJ oldBmp = SelectObject(hMem, hBmp);
|
||||
|
||||
// 高质量缩放:HALFTONE 内插
|
||||
SetStretchBltMode(hMem, HALFTONE);
|
||||
SetBrushOrgEx(hMem, 0, 0, NULL);
|
||||
BOOL bb = StretchBlt(hMem, 0, 0, targetW, targetH,
|
||||
hScreen, 0, 0, srcW, srcH, SRCCOPY | CAPTUREBLT);
|
||||
|
||||
SelectObject(hMem, oldBmp);
|
||||
|
||||
Bitmap* out = nullptr;
|
||||
if (bb) {
|
||||
// 拷贝 HBITMAP 到 GDI+ Bitmap,避免后续释放设备 DC 影响图像
|
||||
Bitmap tmp(hBmp, NULL);
|
||||
if (tmp.GetLastStatus() == Ok) {
|
||||
out = tmp.Clone(0, 0, targetW, targetH, PixelFormat24bppRGB);
|
||||
if (out && out->GetLastStatus() != Ok) {
|
||||
delete out;
|
||||
out = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeleteObject(hBmp);
|
||||
DeleteDC(hMem);
|
||||
ReleaseDC(NULL, hScreen);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight)
|
||||
{
|
||||
out.clear();
|
||||
outWidth = outHeight = 0;
|
||||
|
||||
if (!g_boot.ok) return SCREEN_PREVIEW_NOT_SUPPORTED;
|
||||
if (maxWidth < 64) maxWidth = 64;
|
||||
if (maxWidth > 1920) maxWidth = 1920;
|
||||
if (quality < 1) quality = 1;
|
||||
if (quality > 100) quality = 100;
|
||||
|
||||
int srcW = GetSystemMetrics(SM_CXSCREEN);
|
||||
int srcH = GetSystemMetrics(SM_CYSCREEN);
|
||||
if (srcW <= 0 || srcH <= 0) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
// 等比缩放,禁止放大
|
||||
int targetW = (srcW <= maxWidth) ? srcW : maxWidth;
|
||||
int targetH = (int)((double)srcH * targetW / srcW + 0.5);
|
||||
if (targetH <= 0) targetH = 1;
|
||||
// 偶数对齐,JPEG 编码更高效
|
||||
targetW &= ~1;
|
||||
targetH &= ~1;
|
||||
if (targetW < 2) targetW = 2;
|
||||
if (targetH < 2) targetH = 2;
|
||||
|
||||
Bitmap* bmp = GrabPrimaryScaled(targetW, targetH);
|
||||
if (!bmp) return SCREEN_PREVIEW_CAPTURE_FAILED;
|
||||
|
||||
CLSID clsid;
|
||||
if (GetJpegEncoderClsid(clsid) != 0) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
EncoderParameters params;
|
||||
params.Count = 1;
|
||||
params.Parameter[0].Guid = EncoderQuality;
|
||||
params.Parameter[0].Type = EncoderParameterValueTypeLong;
|
||||
params.Parameter[0].NumberOfValues = 1;
|
||||
ULONG q = (ULONG)quality;
|
||||
params.Parameter[0].Value = &q;
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &stream))) {
|
||||
delete bmp;
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
Status st = bmp->Save(stream, &clsid, ¶ms);
|
||||
delete bmp;
|
||||
|
||||
if (st != Ok) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
HGLOBAL hMem = NULL;
|
||||
if (FAILED(GetHGlobalFromStream(stream, &hMem)) || !hMem) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
SIZE_T sz = GlobalSize(hMem);
|
||||
if (sz == 0) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
|
||||
void* p = GlobalLock(hMem);
|
||||
if (!p) {
|
||||
stream->Release();
|
||||
return SCREEN_PREVIEW_ENCODE_FAILED;
|
||||
}
|
||||
out.assign((unsigned char*)p, (unsigned char*)p + sz);
|
||||
GlobalUnlock(hMem);
|
||||
stream->Release();
|
||||
|
||||
outWidth = targetW;
|
||||
outHeight = targetH;
|
||||
return SCREEN_PREVIEW_OK;
|
||||
}
|
||||
16
client/ScreenPreview.h
Normal file
16
client/ScreenPreview.h
Normal file
@@ -0,0 +1,16 @@
|
||||
// ScreenPreview.h
|
||||
// 屏幕预览:抓主屏 → 等比缩放 → JPEG 编码,供 COMMAND_SCREEN_PREVIEW_REQ 响应使用。
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
// 抓取主屏并编码成 JPEG。
|
||||
// maxWidth 服务端期望的宽度;客户端按源屏宽度等比缩放,不会强制放大
|
||||
// quality JPEG 质量 1..100(建议 40..85)
|
||||
// out 编码后的 JPEG 字节流
|
||||
// outWidth 实际编码图宽
|
||||
// outHeight 实际编码图高
|
||||
// 返回 0 表示成功;非 0 见 ScreenPreviewStatus(枚举在 commands.h)
|
||||
int CaptureAndEncodePreview(int maxWidth, int quality,
|
||||
std::vector<unsigned char>& out,
|
||||
int& outWidth, int& outHeight);
|
||||
@@ -209,6 +209,7 @@
|
||||
<ClCompile Include="reg_startup.c" />
|
||||
<ClCompile Include="SafeThread.cpp" />
|
||||
<ClCompile Include="ScreenManager.cpp" />
|
||||
<ClCompile Include="ScreenPreview.cpp" />
|
||||
<ClCompile Include="ScreenSpy.cpp" />
|
||||
<ClCompile Include="ServicesManager.cpp" />
|
||||
<ClCompile Include="ServiceWrapper.c" />
|
||||
@@ -257,6 +258,7 @@
|
||||
<ClInclude Include="SafeThread.h" />
|
||||
<ClInclude Include="ScreenCapturerDXGI.h" />
|
||||
<ClInclude Include="ScreenManager.h" />
|
||||
<ClInclude Include="ScreenPreview.h" />
|
||||
<ClInclude Include="ScreenSpy.h" />
|
||||
<ClInclude Include="ServicesManager.h" />
|
||||
<ClInclude Include="ServiceWrapper.h" />
|
||||
|
||||
@@ -125,9 +125,10 @@ inline int isValid_10s()
|
||||
#define DLL_VERSION __DATE__ // DLL版本
|
||||
|
||||
// 客户端能力位
|
||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
|
||||
// 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
|
||||
#define CLIENT_CAP_V2 0x0001 // 支持 V2 文件传输
|
||||
#define CLIENT_CAP_UTF8 0x0002 // 协议字符串字段统一使用 UTF-8 编码(活动窗口、窗口列表、键盘记录中的窗口标题等)
|
||||
// 无此位 = 老客户端,按系统 ANSI(默认 CP936)解读
|
||||
#define CLIENT_CAP_SCREEN_PREVIEW 0x0004 // 支持屏幕预览(双击在线主机时弹缩略图)
|
||||
|
||||
#define TALK_DLG_MAXLEN 1024 // 最大输入字符长度
|
||||
|
||||
@@ -333,8 +334,103 @@ enum {
|
||||
CMD_EXECUTE_DLL_NEW = 243, // 执行代码
|
||||
CMD_PEER_TO_PEER = 244, // P2P通信
|
||||
TOKEN_CLIENTID = 245,
|
||||
TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck)
|
||||
COMMAND_SCREEN_PREVIEW_REQ = 247, // 屏幕预览请求(服务端→客户端)
|
||||
TOKEN_SCREEN_PREVIEW_RSP = 248, // 屏幕预览响应(客户端→服务端)
|
||||
};
|
||||
|
||||
// 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID
|
||||
// 钉在子连接 ctx 上,后续命令免 IP 反查直接拿到主连接关联。
|
||||
// 主连接(TOKEN_LOGIN 流)不走此校验。
|
||||
//
|
||||
// 兼容性策略:
|
||||
// - 老客户端不发 → 新服务端宽容(保留 IP 反查兜底,行为不变)。
|
||||
// - 新客户端发出 → 等服务端 ConnAuthAck,超时或失败则不继续。
|
||||
// - 因此新客户端只能向新服务端连接(破坏性升级)。
|
||||
// - 未来收紧:服务端可拒绝所有未通过 auth 的子连接。
|
||||
//
|
||||
// 协议固定为 512 / 256 字节(参照 LOGIN_INFOR::szReserved[512] 的做法),
|
||||
// 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token /
|
||||
// per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段
|
||||
// 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。
|
||||
#pragma pack(push, 1)
|
||||
struct ConnAuthPacket {
|
||||
uint8_t token; // = TOKEN_CONN_AUTH [1]
|
||||
uint64_t clientID; // 客户端 V2 ID(MachineGuid + 归一化路径算出) [8]
|
||||
uint64_t timestamp; // 客户端发包时的 Unix 秒,防重放第一道 [8]
|
||||
uint8_t nonce[16]; // 随机数,进一步降低重放/碰撞概率 [16]
|
||||
char signature[64]; // signMessage("", clientID||timestamp||nonce, 32) [64]
|
||||
char reserved[415]; // 预留扩展 [415]
|
||||
}; // 总 512
|
||||
|
||||
// 服务端响应:token + status + serverTime + 预留,固定 256 字节。
|
||||
// serverTime 客户端可用来校正本机时钟偏差用于后续协议(可选)。
|
||||
struct ConnAuthAck {
|
||||
uint8_t token; // = TOKEN_CONN_AUTH(回显,方便客户端 dispatch) [1]
|
||||
uint8_t status; // 0=OK, 其它=失败原因(见 ConnAuthStatus) [1]
|
||||
uint64_t serverTime; // 服务端处理时的 Unix 秒 [8]
|
||||
char reserved[246]; // 预留扩展 [246]
|
||||
}; // 总 256
|
||||
#pragma pack(pop)
|
||||
|
||||
// 编译期断言:协议大小不允许被无意改动
|
||||
static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes");
|
||||
static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes");
|
||||
|
||||
// 屏幕预览:服务端按双击在线主机触发,向客户端要一张缩略图(JPEG),与浮窗一起显示。
|
||||
// 服务端依 ctx 最近心跳 Ping + RES_RESOLUTION 决定 maxWidth/quality 后下发;客户端
|
||||
// 主屏抓图 → 等比缩放 → JPEG 编码 → 回响应。format 字段 v1 锁 0=JPEG,预留 PNG/WebP。
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenPreviewReq {
|
||||
uint8_t cmd; // = COMMAND_SCREEN_PREVIEW_REQ
|
||||
uint16_t reqId; // 请求序号,用于丢弃过期响应
|
||||
uint16_t maxWidth; // 服务端期望的目标宽度(客户端等比缩放,不强制)
|
||||
uint8_t jpegQuality; // 1..100
|
||||
uint16_t reserved;
|
||||
}; // 总 8 字节
|
||||
|
||||
enum ScreenPreviewStatus {
|
||||
SCREEN_PREVIEW_OK = 0,
|
||||
SCREEN_PREVIEW_CAPTURE_FAILED = 1, // 抓屏失败
|
||||
SCREEN_PREVIEW_ENCODE_FAILED = 2, // 编码失败
|
||||
SCREEN_PREVIEW_NOT_SUPPORTED = 3, // 平台不支持
|
||||
};
|
||||
|
||||
enum ScreenPreviewFormat {
|
||||
SCREEN_PREVIEW_FMT_JPEG = 0,
|
||||
SCREEN_PREVIEW_FMT_PNG = 1, // 预留
|
||||
SCREEN_PREVIEW_FMT_WEBP = 2, // 预留
|
||||
};
|
||||
|
||||
struct ScreenPreviewRspHeader {
|
||||
uint8_t token; // = TOKEN_SCREEN_PREVIEW_RSP
|
||||
uint16_t reqId; // 回显请求序号
|
||||
uint8_t status; // ScreenPreviewStatus
|
||||
uint8_t format; // ScreenPreviewFormat(v1 仅 JPEG)
|
||||
uint16_t width; // 实际编码图宽
|
||||
uint16_t height; // 实际编码图高
|
||||
uint32_t bytes; // 图像字节数(紧随其后)
|
||||
uint8_t reserved[3];
|
||||
// 后接 data[bytes]
|
||||
}; // 头部 16 字节
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(ScreenPreviewReq) == 8, "ScreenPreviewReq must be 8 bytes");
|
||||
static_assert(sizeof(ScreenPreviewRspHeader) == 16, "ScreenPreviewRspHeader must be 16 bytes");
|
||||
|
||||
enum ConnAuthStatus {
|
||||
CONN_AUTH_OK = 0,
|
||||
CONN_AUTH_BAD_SIZE = 1, // 包长度不对
|
||||
CONN_AUTH_CLOCK_SKEW = 2, // 时间戳超过容忍范围
|
||||
CONN_AUTH_BAD_SIGNATURE = 3, // HMAC 不匹配
|
||||
CONN_AUTH_INTERNAL_ERROR = 4,
|
||||
};
|
||||
|
||||
#define CONN_AUTH_TIMESTAMP_TOLERANCE_SEC 300 // 客户端/服务端时钟漂移容忍 ±5 分钟
|
||||
#define CONN_AUTH_CLIENT_WAIT_MS 10000 // 客户端等待 ack 的超时
|
||||
// 设为 10 秒留足跨太平洋 + 拥塞 / 卫星链路 / 偏远网络的余量;
|
||||
// 同机几毫秒就回,正常路径用户感知不到。
|
||||
|
||||
enum MachineCommand {
|
||||
MACHINE_LOGOUT,
|
||||
MACHINE_SHUTDOWN,
|
||||
@@ -918,7 +1014,7 @@ typedef struct LOGIN_INFOR {
|
||||
{
|
||||
memset(this, 0, sizeof(LOGIN_INFOR));
|
||||
bToken = TOKEN_LOGIN;
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8);
|
||||
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW);
|
||||
}
|
||||
LOGIN_INFOR& Speed(unsigned long speed)
|
||||
{
|
||||
@@ -1091,6 +1187,29 @@ inline const QualityProfile& GetQualityProfile(int level) {
|
||||
return g_QualityProfiles[level];
|
||||
}
|
||||
|
||||
// 屏幕预览质量配置(与 QualityLevel 共用 RTT 阈值表,但参数维度不同:缩略图只关心
|
||||
// 编码尺寸 + JPEG 质量,没有 FPS / 算法等运动视频参数)
|
||||
struct PreviewProfile {
|
||||
int maxWidth; // 期望编码宽度(客户端会等比缩放,禁止放大)
|
||||
int jpegQuality; // JPEG 质量 1..100
|
||||
};
|
||||
|
||||
inline const PreviewProfile& GetScreenPreviewProfile(int level) {
|
||||
static const PreviewProfile g_PreviewProfiles[QUALITY_COUNT] = {
|
||||
{ 1024, 85 }, // Ultra: 超清 (LAN/同省,4K 源屏可进一步放大到 1280)
|
||||
{ 800, 80 }, // High: 高清 (跨省直连)
|
||||
{ 640, 75 }, // Good: 标清 (同国/邻国)
|
||||
{ 480, 70 }, // Medium: 常规 (大陆间)
|
||||
{ 384, 60 }, // Low: 低清 (跨洲)
|
||||
{ 256, 50 }, // Minimal: 最低 (极差网络/卫星链路)
|
||||
};
|
||||
if (level < 0 || level >= QUALITY_COUNT) {
|
||||
static const PreviewProfile fallback = { 480, 70 };
|
||||
return fallback;
|
||||
}
|
||||
return g_PreviewProfiles[level];
|
||||
}
|
||||
|
||||
// 根据RTT获取目标质量等级 (控制端使用)
|
||||
inline int GetTargetQualityLevel(int rtt, int usingFRP) {
|
||||
// 根据模式应用不同 RTT阈值 (毫秒)
|
||||
|
||||
@@ -347,6 +347,8 @@ void* ShellworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -371,6 +373,8 @@ void* ScreenworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -395,6 +399,8 @@ void* SystemManagerThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<SystemManager> handler(new SystemManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -417,6 +423,8 @@ void* FileManagerThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -690,6 +698,49 @@ std::string getUsername()
|
||||
return u ? u : "?";
|
||||
}
|
||||
|
||||
// 读取 systemd / dbus 维护的 machine-id(与 Windows MachineGuid 等价)
|
||||
// /etc/machine-id 在系统首次启动时生成的随机 32 字符 hex GUID。
|
||||
// 对应 Windows: HKLM\Software\Microsoft\Cryptography\MachineGuid。
|
||||
// 重装系统才会变;同一镜像 dd 出来的多机会撞——但规范的批量部署
|
||||
// 工具 (cloud-init / kickstart) 会重置它。
|
||||
static std::string getMachineId()
|
||||
{
|
||||
// 优先 /etc/machine-id;某些精简系统可能放在 /var/lib/dbus/machine-id
|
||||
const char* paths[] = { "/etc/machine-id", "/var/lib/dbus/machine-id" };
|
||||
for (const char* p : paths) {
|
||||
std::ifstream f(p);
|
||||
if (!f.is_open()) continue;
|
||||
std::string id;
|
||||
std::getline(f, id);
|
||||
// 去掉尾部空白和换行
|
||||
while (!id.empty() && (id.back() == '\n' || id.back() == '\r' ||
|
||||
id.back() == ' ' || id.back() == '\t')) {
|
||||
id.pop_back();
|
||||
}
|
||||
if (!id.empty()) return id;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// 路径归一化(Linux 版):解析符号链接 + 转小写
|
||||
// realpath 等价于 Windows 的 GetLongPathName,把 /usr/local/bin/../foo 这种
|
||||
// 折回到规范形式;小写化避免大小写差异引起 ID 不同(Linux 文件系统本身大小写
|
||||
// 敏感,但保持与 Windows V2 算法一致的归一化策略,跨端一致性优先)。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败(罕见):用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 获取屏幕分辨率字符串(格式 "显示器数:宽*高")
|
||||
std::string getScreenResolution()
|
||||
{
|
||||
@@ -1010,17 +1061,30 @@ int main(int argc, char* argv[])
|
||||
logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME
|
||||
logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN
|
||||
logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION
|
||||
// 计算客户端 ID(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// 格式: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
distro + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str());
|
||||
// V2 ID 算法:machine-id + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/distro/CPU 漂移)
|
||||
// - 局域网多机即便同镜像,cloud-init/kickstart 会让 machine-id 各不同
|
||||
// - machine-id 读取失败时退化到老算法(pubIP|hostname|distro|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID(v2): %llu (machineId=%s, path=%s)\n",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底(与服务端 CONTEXT_OBJECT::CalculateID 相同算法)
|
||||
// 格式: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
distro + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
Mprintf("Calculated clientID(v1 fallback): %llu (machine-id 读取失败)\n", g_myClientID);
|
||||
}
|
||||
|
||||
logInfo.AddReserved(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID
|
||||
logInfo.AddReserved((int)getpid()); // [17] RES_PID
|
||||
|
||||
@@ -214,6 +214,54 @@ static std::string getUsername()
|
||||
return user ? std::string(user) : "unknown";
|
||||
}
|
||||
|
||||
// 读取 IOKit 维护的 IOPlatformUUID(与 Windows MachineGuid 等价)
|
||||
// 这是主板/系统级 UUID,由 IOPlatformExpertDevice 服务提供,重装系统通常不变。
|
||||
// 对应:Windows HKLM\Software\Microsoft\Cryptography\MachineGuid
|
||||
// Linux /etc/machine-id
|
||||
static std::string getMachineId()
|
||||
{
|
||||
std::string result;
|
||||
io_service_t platformExpert = IOServiceGetMatchingService(
|
||||
kIOMasterPortDefault,
|
||||
IOServiceMatching("IOPlatformExpertDevice"));
|
||||
if (platformExpert != IO_OBJECT_NULL) {
|
||||
CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty(
|
||||
platformExpert, CFSTR(kIOPlatformUUIDKey),
|
||||
kCFAllocatorDefault, 0);
|
||||
if (uuidProperty != nullptr) {
|
||||
if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) {
|
||||
CFStringRef uuidStr = (CFStringRef)uuidProperty;
|
||||
char buf[64] = {};
|
||||
if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) {
|
||||
result = buf;
|
||||
}
|
||||
}
|
||||
CFRelease(uuidProperty);
|
||||
}
|
||||
IOObjectRelease(platformExpert);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 路径归一化(macOS 版):解析符号链接 + 转小写
|
||||
// realpath 把 /Applications/foo/../bar 之类折回规范形式;
|
||||
// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感,
|
||||
// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。
|
||||
static std::string normalizeExePathLower(const std::string& path)
|
||||
{
|
||||
char resolved[PATH_MAX] = {};
|
||||
std::string out;
|
||||
if (realpath(path.c_str(), resolved) != nullptr) {
|
||||
out = resolved;
|
||||
} else {
|
||||
out = path; // 解析失败:用原值
|
||||
}
|
||||
for (auto& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Get screen resolution
|
||||
static std::string getScreenResolution()
|
||||
{
|
||||
@@ -552,16 +600,30 @@ static void fillLoginInfo(LOGIN_INFOR& info)
|
||||
std::string resolution = getScreenResolution();
|
||||
info.AddReserved(resolution.c_str());
|
||||
|
||||
// 17. Client ID (calculated from system info, same algorithm as server)
|
||||
// Format: pubIP|hostname|os|cpu|path
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
osVer + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
// 17. Client ID
|
||||
// V2 算法:IOPlatformUUID + 归一化路径
|
||||
// - 同机同程序路径永远同 ID(不依赖 IP/hostname/os/CPU 漂移)
|
||||
// - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同
|
||||
// - 读取失败时退化到老算法(pubIP|hostname|os|cpu|path)保兼容
|
||||
std::string machineId = getMachineId();
|
||||
if (!machineId.empty()) {
|
||||
std::string normPath = normalizeExePathLower(exePath);
|
||||
std::string idInput = machineId + "|" + normPath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)",
|
||||
g_myClientID, machineId.c_str(), normPath.c_str());
|
||||
} else {
|
||||
// 老算法兜底
|
||||
char cpuStr[32];
|
||||
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
|
||||
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
|
||||
hostname + "|" +
|
||||
osVer + "|" +
|
||||
cpuStr + "|" +
|
||||
exePath;
|
||||
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
|
||||
NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID);
|
||||
}
|
||||
info.AddReserved(std::to_string(g_myClientID).c_str());
|
||||
|
||||
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
|
||||
@@ -651,6 +713,8 @@ void* ShellworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
@@ -675,6 +739,8 @@ void* ScreenworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
|
||||
if (!handler->init()) {
|
||||
@@ -703,6 +769,8 @@ void* FileManagerworkingThread(void* param)
|
||||
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
|
||||
void* clientAddr = ClientObject.get();
|
||||
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
|
||||
// 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。
|
||||
ClientObject->EnableSubConnAuth(true, g_myClientID);
|
||||
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
|
||||
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
|
||||
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "PluginSettingsDlg.h"
|
||||
#include "TriggerSettingsDlg.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
#include "common/key.h"
|
||||
#include "UIBranding.h"
|
||||
|
||||
@@ -86,6 +87,7 @@
|
||||
#define TIMER_REFRESH_LIST 5
|
||||
#define TIMER_STATUSBAR_UPDATE 6
|
||||
#define TIMER_STATUSBAR_INIT 7
|
||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||
#define TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
|
||||
#define TINY_DLL_NAME "TinyRun.dll"
|
||||
#define FRPC_DLL_NAME "Frpc.dll"
|
||||
@@ -739,7 +741,11 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
// 启用 LVM_SETUNICODEFORMAT 后,列表实际发送的是 LVN_GETDISPINFOW(即便工程是 MBCS)。
|
||||
// MBCS 工程里 LVN_GETDISPINFO == LVN_GETDISPINFOA,两者码值不同,需各自映射。
|
||||
ON_NOTIFY(LVN_GETDISPINFOW, IDC_ONLINE, &CMy2015RemoteDlg::OnGetDispInfoW)
|
||||
// m_CList_Online 启用 LVM_SETUNICODEFORMAT(TRUE) 后,列头会发 HDN_ITEMCLICKW;
|
||||
// MBCS 工程里 HDN_ITEMCLICK == HDN_ITEMCLICKA,码值跟 W 版不同,必须各自映射,
|
||||
// 否则点表头排序失效。两条都注册到同一个处理函数。
|
||||
ON_NOTIFY(HDN_ITEMCLICK, 0, &CMy2015RemoteDlg::OnHdnItemclickList)
|
||||
ON_NOTIFY(HDN_ITEMCLICKW, 0, &CMy2015RemoteDlg::OnHdnItemclickList)
|
||||
ON_COMMAND(ID_ONLINE_MESSAGE, &CMy2015RemoteDlg::OnOnlineMessage)
|
||||
ON_COMMAND(ID_ONLINE_DELETE, &CMy2015RemoteDlg::OnOnlineDelete)
|
||||
ON_COMMAND(ID_ONLINE_UPDATE, &CMy2015RemoteDlg::OnOnlineUpdate)
|
||||
@@ -795,6 +801,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient)
|
||||
ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient)
|
||||
ON_MESSAGE(WM_UPDATE_ACTIVEWND, UpdateUserEvent)
|
||||
ON_MESSAGE(WM_PREVIEW_RESPONSE, OnPreviewResponse)
|
||||
ON_WM_HELPINFO()
|
||||
ON_COMMAND(ID_ONLINE_SHARE, &CMy2015RemoteDlg::OnOnlineShare)
|
||||
ON_COMMAND(ID_TOOL_AUTH, &CMy2015RemoteDlg::OnToolAuth)
|
||||
@@ -1144,19 +1151,18 @@ VOID CMy2015RemoteDlg::CreatStatusBar()
|
||||
|
||||
VOID CMy2015RemoteDlg::CreateNotifyBar()
|
||||
{
|
||||
// GUID 用于 Windows 10/11 Toast 通知关联托盘图标
|
||||
// {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
|
||||
static const GUID NOTIFY_ICON_GUID =
|
||||
{ 0xA1B2C3D4, 0xE5F6, 0x7890, { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x90 } };
|
||||
|
||||
m_Nid.uVersion = NOTIFYICON_VERSION_4;
|
||||
m_Nid.cbSize = sizeof(NOTIFYICONDATA); //大小赋值
|
||||
m_Nid.hWnd = m_hWnd; //父窗口 是被定义在父类CWnd类中
|
||||
m_Nid.uID = IDR_MAINFRAME; //icon ID
|
||||
m_Nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_GUID; //托盘所拥有的状态
|
||||
// 注意:不加 NIF_GUID。NIF_GUID 会把托盘图标的注册和 EXE 完整路径绑死
|
||||
// (MSDN:If a Shell_NotifyIcon call uses a GUID that is recognized as
|
||||
// belonging to a different application path, the call will fail),
|
||||
// 导致 Debug 和 Release 编译产物(路径不同)相互冲突——先注册的占住 GUID,
|
||||
// 后启动的 NIM_ADD 静默失败、托盘没图标。
|
||||
m_Nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; //托盘所拥有的状态
|
||||
m_Nid.uCallbackMessage = UM_ICONNOTIFY; //回调消息
|
||||
m_Nid.hIcon = m_hIcon; //icon 变量
|
||||
m_Nid.guidItem = NOTIFY_ICON_GUID;
|
||||
|
||||
// 先删除可能残留的旧图标(程序异常退出时可能残留)
|
||||
Shell_NotifyIcon(NIM_DELETE, &m_Nid);
|
||||
@@ -1342,15 +1348,25 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
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,
|
||||
};
|
||||
auto id = CONTEXT_OBJECT::CalculateID(data);
|
||||
auto id_str = std::to_string(id);
|
||||
if (v[RES_CLIENT_ID].empty()) {
|
||||
v[RES_CLIENT_ID] = id_str;
|
||||
} else if (id_str != v[RES_CLIENT_ID]) {
|
||||
Mprintf("上线消息 - 主机ID错误: calc=%llu, recv=%s, IP=%s, Path=%s\n",
|
||||
id, v[RES_CLIENT_ID].c_str(), strIP.GetString(), path.GetString());
|
||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
||||
// 客户端未发或解析失败时,回退到服务端老算法重算(兼容老客户端)。
|
||||
auto computedId = CONTEXT_OBJECT::CalculateID(data);
|
||||
uint64_t id = 0;
|
||||
if (!v[RES_CLIENT_ID].empty()) {
|
||||
id = std::strtoull(v[RES_CLIENT_ID].c_str(), nullptr, 10);
|
||||
}
|
||||
if (id == 0) {
|
||||
id = computedId;
|
||||
v[RES_CLIENT_ID] = std::to_string(id);
|
||||
}
|
||||
|
||||
bool modify = false, needConvert = true;
|
||||
|
||||
// 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。
|
||||
if (TryMigrateClientMetadata(id, strPCName, path)) {
|
||||
modify = true;
|
||||
}
|
||||
CString loc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
|
||||
if (loc.IsEmpty()) {
|
||||
loc = v[RES_CLIENT_LOC].c_str();
|
||||
@@ -3026,6 +3042,12 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
if (nIDEvent == TIMER_CLOSEWND) {
|
||||
DeletePopupWindow();
|
||||
}
|
||||
if (nIDEvent == TIMER_PREVIEW_ARRIVAL) {
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
if (m_pPreviewTip && ::IsWindow(m_pPreviewTip->GetSafeHwnd())) {
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
}
|
||||
}
|
||||
if (nIDEvent == TIMER_CLEAR_BALLOON) {
|
||||
KillTimer(TIMER_CLEAR_BALLOON);
|
||||
|
||||
@@ -3341,7 +3363,10 @@ void CMy2015RemoteDlg::DeletePopupWindow(BOOL bForce)
|
||||
m_pFloatingTip->DestroyWindow();
|
||||
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
m_pPreviewTip = nullptr;
|
||||
m_PreviewReqId = 0;
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
}
|
||||
|
||||
|
||||
@@ -3476,10 +3501,17 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
|
||||
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
*pResult = 0;
|
||||
// ON_NOTIFY(HDN_ITEMCLICK, 0, ...) 的 ID=0 匹配的是 listview 内部 header 控件 ID,
|
||||
// 而 m_CList_Online 和 m_CList_Message 的 header 内部 ID 都是 0,导致两边表头点击
|
||||
// 都进这个回调(老 bug)。只处理在线主机列表的 header,避免点日志列表表头串到主机排序。
|
||||
HWND hOnlineHeader = ListView_GetHeader(m_CList_Online.GetSafeHwnd());
|
||||
if (pNMHDR->hwndFrom != hOnlineHeader) {
|
||||
return;
|
||||
}
|
||||
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
|
||||
int nColumn = pNMHeader->iItem; // 获取点击的列索引
|
||||
SortByColumn(nColumn); // 调用排序函数
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 虚拟列表数据回调 - 当列表需要显示某行某列的数据时调用
|
||||
@@ -5418,6 +5450,15 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
case 137: // 心跳【L】
|
||||
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject);
|
||||
break;
|
||||
case TOKEN_SCREEN_PREVIEW_RSP: {
|
||||
// 屏幕预览响应:把整个包拷到堆上,转给主线程处理(IO 线程不接触 UI)
|
||||
if (len < sizeof(ScreenPreviewRspHeader)) break;
|
||||
auto* pkt = new std::vector<BYTE>(szBuffer, szBuffer + len);
|
||||
if (!g_2015RemoteDlg->PostMessageA(WM_PREVIEW_RESPONSE, 0, (LPARAM)pkt)) {
|
||||
delete pkt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SOCKET_DLLLOADER: {// 请求DLL【L】
|
||||
auto len = ContextObject->InDeCompressedBuffer.GetBufferLength();
|
||||
bool is64Bit = len > 1 ? ContextObject->InDeCompressedBuffer.GetBYTE(1) : false;
|
||||
@@ -5484,6 +5525,53 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
g_2015RemoteDlg->SendMessage(WM_USERTOONLINELIST, 0, (LPARAM)ContextObject);
|
||||
break;
|
||||
}
|
||||
case TOKEN_CONN_AUTH: { // 子连接身份校验【L】
|
||||
// 设计取舍:
|
||||
// - 主连接走 TOKEN_LOGIN,不进入此分支。
|
||||
// - 当前阶段宽容:未通过的子连接仍允许后续命令(依靠 IP 反查兜底),
|
||||
// 仅把 IsAuthenticated 标志记下来,便于后续命令优先用。
|
||||
// - 失败时回应 ConnAuthAck 让客户端 fail fast,但不主动断连接
|
||||
// (客户端拒绝继续 = 自己关闭,等价效果,少一次 RST 噪音)。
|
||||
ConnAuthAck ack = {};
|
||||
ack.token = TOKEN_CONN_AUTH;
|
||||
ack.serverTime = (uint64_t)time(0);
|
||||
ack.status = CONN_AUTH_INTERNAL_ERROR;
|
||||
|
||||
if (len < (int)sizeof(ConnAuthPacket)) {
|
||||
ack.status = CONN_AUTH_BAD_SIZE;
|
||||
Mprintf("[ConnAuth] %s: 包长度不足 (%d < %zu)\n",
|
||||
ContextObject->GetPeerName().c_str(), len, sizeof(ConnAuthPacket));
|
||||
} else {
|
||||
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
|
||||
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
|
||||
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
|
||||
ack.status = CONN_AUTH_CLOCK_SKEW;
|
||||
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
|
||||
ContextObject->GetPeerName().c_str(), skew);
|
||||
} else {
|
||||
BYTE sigInput[8 + 8 + 16];
|
||||
memcpy(sigInput, &pkt->clientID, 8);
|
||||
memcpy(sigInput + 8, &pkt->timestamp, 8);
|
||||
memcpy(sigInput + 16, pkt->nonce, 16);
|
||||
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
|
||||
std::string sig(pkt->signature, pkt->signature + 64);
|
||||
if (verifyMessage("", sigInput, sizeof(sigInput), sig)) {
|
||||
// 通过:把 clientID 钉在子连接 ctx 上
|
||||
ContextObject->SetID(pkt->clientID);
|
||||
ContextObject->SetAuthenticated(true);
|
||||
ack.status = CONN_AUTH_OK;
|
||||
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
|
||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||
} else {
|
||||
ack.status = CONN_AUTH_BAD_SIGNATURE;
|
||||
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
|
||||
ContextObject->GetPeerName().c_str(), pkt->clientID);
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextObject->Send2Client((PBYTE)&ack, sizeof(ack));
|
||||
break;
|
||||
}
|
||||
case TOKEN_BITMAPINFO: { // 远程桌面【x】
|
||||
ContextObject->SetNoDelay(TRUE);
|
||||
ContextObject->EnableZstdContext(-1);
|
||||
@@ -5612,6 +5700,65 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
|
||||
|
||||
// 启发式 ID 迁移:当新客户端 V2 算法生成的 ID 在 m_ClientMap 里没条目时,
|
||||
// 按 (ComputerName, ProgramPath) 扫老条目找唯一匹配,把元数据搬过去。
|
||||
// 设计取舍见相关讨论:
|
||||
// - 严格 ComputerName 相等 + 大小写不敏感 ProgramPath 匹配。
|
||||
// - 多候选保守跳过——避免局域网同 hostname+同路径多机互相串备注。
|
||||
// - 老条目不删,作为审计/回滚依据;dat 文件不会显著膨胀。
|
||||
//
|
||||
// 线程安全说明:
|
||||
// - AddList 由 SendMessage(WM_USERTOONLINELIST) 进入,跑在 UI 线程,串行。
|
||||
// - newId 在本函数返回前没进入 m_HostList,UI 上看不到这台机器,
|
||||
// 用户无法通过右键触发 OnOnlineHostnote 写 newId 的备注。
|
||||
// - IO 线程的 AUTH 写发生在登录认证流之后,针对的是已注册客户端,不会
|
||||
// 在 newId 首次出现的瞬间踩进来。
|
||||
// - GetAll() 返回快照副本,迭代期间老条目被并发修改不影响匹配(我们只
|
||||
// 看 ComputerName/ProgramPath,这两个字段不会被并发改写)。
|
||||
// - _ClientList 实现内部有同步(项目里其它路径同样不持 m_cs 调用它)。
|
||||
// 故无需额外加锁。
|
||||
bool CMy2015RemoteDlg::TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath)
|
||||
{
|
||||
if (!m_ClientMap) return false;
|
||||
if (m_ClientMap->Exists(newId)) return false; // 已有条目,无需迁移
|
||||
|
||||
const std::string targetPC = pcName.GetString();
|
||||
const std::string targetPath = exePath.GetString();
|
||||
|
||||
// 扫描所有条目,找匹配的老 ID(多于一个就停,按歧义处理)
|
||||
std::vector<ClientKey> candidates;
|
||||
for (const auto& kv : m_ClientMap->GetAll()) {
|
||||
if (kv.first == newId) continue;
|
||||
if (strcmp(kv.second.ComputerName, targetPC.c_str()) == 0 &&
|
||||
_stricmp(kv.second.ProgramPath, targetPath.c_str()) == 0) {
|
||||
candidates.push_back(kv.first);
|
||||
if (candidates.size() > 1) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size() > 1) {
|
||||
Mprintf("ID 迁移歧义: PC=%s, Path=%s 命中 %zu+ 个候选,保守跳过——"
|
||||
"需要运维手动确认是哪台机器的元数据\n",
|
||||
targetPC.c_str(), targetPath.c_str(), candidates.size());
|
||||
return false;
|
||||
}
|
||||
if (candidates.empty()) return false; // 真正的新机器
|
||||
|
||||
// 唯一匹配:复制用户可设置的元数据到新 ID(其它字段下次心跳/上线会覆盖)
|
||||
ClientKey oldId = candidates[0];
|
||||
m_ClientMap->SetClientMapData(newId, MAP_NOTE,
|
||||
m_ClientMap->GetClientMapData(oldId, MAP_NOTE).GetString());
|
||||
m_ClientMap->SetClientMapData(newId, MAP_LOCATION,
|
||||
m_ClientMap->GetClientMapData(oldId, MAP_LOCATION).GetString());
|
||||
m_ClientMap->SetClientMapInteger(newId, MAP_LEVEL,
|
||||
m_ClientMap->GetClientMapInteger(oldId, MAP_LEVEL));
|
||||
m_ClientMap->SetClientMapInteger(newId, MAP_AUTH,
|
||||
m_ClientMap->GetClientMapInteger(oldId, MAP_AUTH));
|
||||
Mprintf("ID 迁移: %llu -> %llu (PC=%s, Path=%s)\n",
|
||||
oldId, newId, targetPC.c_str(), targetPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据列表显示索引获取 context(考虑分组过滤)
|
||||
context* CMy2015RemoteDlg::GetContextByListIndex(int iItem)
|
||||
{
|
||||
@@ -5625,17 +5772,17 @@ context* CMy2015RemoteDlg::GetContextByListIndex(int iItem)
|
||||
return m_HostList[realIdx];
|
||||
}
|
||||
|
||||
// 从 m_HostList 中移除 context 并更新索引映射
|
||||
// 从 m_HostList 中移除 context 并更新索引映射。
|
||||
//
|
||||
// 重要:MarkDeviceOffline / m_ActiveWndW.erase 等"主机下线"副作用必须**只在
|
||||
// 确实从 m_HostList 移除后**才触发。否则在子连接(auth 通过后 ctx->GetClientID()
|
||||
// 等于主连接 ID)正常断开时,会误把主连接当成下线,造成 Web 端"假下线"和
|
||||
// 活动窗口缓存被清空。
|
||||
bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
||||
{
|
||||
if (!ctx) return false;
|
||||
uint64_t clientID = ctx->GetClientID();
|
||||
// 通知 Web 服务(批量通知,由定时器触发)
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().MarkDeviceOffline(clientID);
|
||||
}
|
||||
// 清理"活动窗口"列的宽字符旁路表
|
||||
m_ActiveWndW.erase(clientID);
|
||||
bool removed = false;
|
||||
|
||||
// 方案1:通过索引快速查找(如果索引有效且匹配)
|
||||
auto indexIt = m_ClientIndex.find(clientID);
|
||||
@@ -5652,29 +5799,40 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
|
||||
m_ClientIndex[c->GetClientID()] = i;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方案2:索引不存在或不匹配,遍历查找(处理重复 ID 的情况)
|
||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||
if (m_HostList[i] == ctx) {
|
||||
m_HostList.erase(m_HostList.begin() + i);
|
||||
// 如果索引指向的是被删除的元素,也删除索引
|
||||
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
|
||||
m_ClientIndex.erase(indexIt);
|
||||
}
|
||||
// 更新后续元素的索引
|
||||
for (size_t j = i; j < m_HostList.size(); ++j) {
|
||||
context* c = m_HostList[j];
|
||||
if (c) {
|
||||
m_ClientIndex[c->GetClientID()] = j;
|
||||
if (!removed) {
|
||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||
if (m_HostList[i] == ctx) {
|
||||
m_HostList.erase(m_HostList.begin() + i);
|
||||
// 如果索引指向的是被删除的元素,也删除索引
|
||||
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
|
||||
m_ClientIndex.erase(indexIt);
|
||||
}
|
||||
// 更新后续元素的索引
|
||||
for (size_t j = i; j < m_HostList.size(); ++j) {
|
||||
context* c = m_HostList[j];
|
||||
if (c) {
|
||||
m_ClientIndex[c->GetClientID()] = j;
|
||||
}
|
||||
}
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
// 副作用:仅在主连接真的从列表中移除时才触发,避免子连接断开误伤主连接的状态。
|
||||
if (removed) {
|
||||
if (WebService().IsRunning()) {
|
||||
WebService().MarkDeviceOffline(clientID);
|
||||
}
|
||||
m_ActiveWndW.erase(clientID);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
|
||||
@@ -7421,36 +7579,141 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
CPoint pt;
|
||||
GetCursorPos(&pt);
|
||||
|
||||
// 清理旧提示
|
||||
// 清理旧提示(DeletePopupWindow 会同时清掉 m_pPreviewTip / m_PreviewReqId / 定时器)
|
||||
DeletePopupWindow(TRUE);
|
||||
|
||||
// 创建提示窗口
|
||||
m_pFloatingTip = new CWnd();
|
||||
int width = res[RES_FILE_PATH].GetLength() * 10 + 36;
|
||||
width = min(max(width, 360), 800);
|
||||
CRect rect(pt.x, pt.y, pt.x + width, pt.y + 120); // 宽度、高度
|
||||
// 屏幕预览触发条件:客户端支持能力位 + 当前活动窗口非 Locked/Inactive
|
||||
CString activeWnd = ctx->GetClientData(ONLINELIST_LOGINTIME);
|
||||
bool isIdleOrLocked =
|
||||
activeWnd.Find(_T("Locked")) == 0 || activeWnd.Find(_T("Inactive")) == 0;
|
||||
bool wantPreview = ctx->SupportsScreenPreview() && !isIdleOrLocked;
|
||||
|
||||
BOOL bOk = m_pFloatingTip->CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
_T("STATIC"),
|
||||
strText,
|
||||
WS_POPUP | WS_VISIBLE | WS_BORDER | SS_LEFT | SS_NOTIFY,
|
||||
rect,
|
||||
this,
|
||||
0);
|
||||
// 创建新预览浮窗
|
||||
WORD maxWidth = 480;
|
||||
BYTE jpegQ = 70;
|
||||
if (wantPreview) {
|
||||
ChooseScreenPreviewParams(ctx, maxWidth, jpegQ);
|
||||
}
|
||||
|
||||
auto* tip = new CPreviewTipWnd();
|
||||
// 项目是 MBCS(CP_ACP),浮窗内部用宽字符渲染(避免跨语言系统乱码)。
|
||||
// 这里把 strText (CStringA) 按 CP_ACP 转 wide。
|
||||
CStringW textW;
|
||||
{
|
||||
int wlen = MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, NULL, 0);
|
||||
if (wlen > 1) {
|
||||
MultiByteToWideChar(CP_ACP, 0, strText.GetString(), -1, textW.GetBufferSetLength(wlen - 1), wlen);
|
||||
textW.ReleaseBuffer(wlen - 1);
|
||||
}
|
||||
}
|
||||
BOOL bOk = tip->Create(this, pt, textW, wantPreview ? maxWidth : 0);
|
||||
if (bOk) {
|
||||
m_pFloatingTip->SetFont(GetFont());
|
||||
m_pFloatingTip->ShowWindow(SW_SHOW);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
m_pFloatingTip = tip;
|
||||
m_pPreviewTip = tip;
|
||||
// 整体超时:有预览 8 秒(图到达后会续命 5s),纯文本 5 秒
|
||||
SetTimer(TIMER_CLOSEWND, wantPreview ? 8000 : 5000, nullptr);
|
||||
|
||||
if (wantPreview) {
|
||||
// 序号 0 视为"无效",从 1 开始递增
|
||||
if (++m_PreviewReqId == 0) m_PreviewReqId = 1;
|
||||
tip->SetReqId(m_PreviewReqId);
|
||||
SendScreenPreviewRequest(ctx, m_PreviewReqId, maxWidth, jpegQ);
|
||||
// 4 秒未收到 → 在浮窗里标"预览不可用"
|
||||
SetTimer(TIMER_PREVIEW_ARRIVAL, 4000, nullptr);
|
||||
}
|
||||
} else {
|
||||
SAFE_DELETE(m_pFloatingTip);
|
||||
delete tip;
|
||||
}
|
||||
}
|
||||
|
||||
*pResult = 0;
|
||||
}
|
||||
|
||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
||||
{
|
||||
int ping = 0;
|
||||
{
|
||||
CString p = ctx->GetClientData(ONLINELIST_PING);
|
||||
if (!p.IsEmpty()) ping = atoi(p.GetString());
|
||||
}
|
||||
// ping 未上报(0/缺失)→ 当中等档处理,避免误判为局域网刷过大缩略图
|
||||
int rttForLevel = ping > 0 ? ping : 250;
|
||||
|
||||
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
||||
const PreviewProfile& prof = GetScreenPreviewProfile(level);
|
||||
int base = prof.maxWidth;
|
||||
BYTE q = (BYTE)prof.jpegQuality;
|
||||
|
||||
// 4K/超宽屏自适应(仅 Ultra/High 启用,弱网档不放大保带宽)
|
||||
if (level <= QUALITY_HIGH) {
|
||||
CString resStr = ctx->GetAdditionalData(RES_RESOLUTION);
|
||||
if (!resStr.IsEmpty()) {
|
||||
int sw = 0, sh = 0;
|
||||
if (_stscanf_s(resStr, _T("%dx%d"), &sw, &sh) == 2 && sw >= 2560) {
|
||||
int adj = sw / 4;
|
||||
if (adj > 1280) adj = 1280;
|
||||
if (adj > base) base = adj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = (WORD)base;
|
||||
jpegQuality = q;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality)
|
||||
{
|
||||
if (!ctx) return;
|
||||
ScreenPreviewReq req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.cmd = COMMAND_SCREEN_PREVIEW_REQ;
|
||||
req.reqId = reqId;
|
||||
req.maxWidth = maxWidth;
|
||||
req.jpegQuality = jpegQuality;
|
||||
ctx->Send2Client((PBYTE)&req, sizeof(req));
|
||||
}
|
||||
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程)
|
||||
// wParam: 未使用(避免依赖 WPARAM 宽度,Win32 上 32 位会截 64 位 clientID)
|
||||
// LPARAM: 指向堆分配的 std::vector<BYTE>*(含完整响应包:[Header|JPEG]),处理完必须 delete。
|
||||
// 校验思路:reqId 由对话框单调递增、每次双击重置 + 同时只有 1 个 in-flight tip,足以
|
||||
// 过滤过期/乱串响应,无需再叠加 clientID 校验。
|
||||
LRESULT CMy2015RemoteDlg::OnPreviewResponse(WPARAM /*wParam*/, LPARAM lParam)
|
||||
{
|
||||
auto* pkt = reinterpret_cast<std::vector<BYTE>*>(lParam);
|
||||
if (!pkt) return 0;
|
||||
std::unique_ptr<std::vector<BYTE>> guard(pkt);
|
||||
|
||||
if (pkt->size() < sizeof(ScreenPreviewRspHeader)) return 0;
|
||||
const ScreenPreviewRspHeader* hdr = reinterpret_cast<const ScreenPreviewRspHeader*>(pkt->data());
|
||||
if (hdr->token != TOKEN_SCREEN_PREVIEW_RSP) return 0;
|
||||
|
||||
// 序号校验:和当前期待的 reqId 不符 → 过期响应,丢弃
|
||||
if (m_PreviewReqId == 0 || hdr->reqId != m_PreviewReqId) return 0;
|
||||
if (!m_pPreviewTip || !::IsWindow(m_pPreviewTip->GetSafeHwnd())) return 0;
|
||||
|
||||
KillTimer(TIMER_PREVIEW_ARRIVAL);
|
||||
|
||||
if (hdr->status != SCREEN_PREVIEW_OK || hdr->bytes == 0 ||
|
||||
pkt->size() < sizeof(ScreenPreviewRspHeader) + hdr->bytes ||
|
||||
hdr->format != SCREEN_PREVIEW_FMT_JPEG)
|
||||
{
|
||||
m_pPreviewTip->MarkPreviewUnavailable();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const BYTE* jpeg = pkt->data() + sizeof(ScreenPreviewRspHeader);
|
||||
m_pPreviewTip->SetImageFromJpeg(jpeg, hdr->bytes);
|
||||
|
||||
// 图到达后续命 5 秒
|
||||
KillTimer(TIMER_CLOSEWND);
|
||||
SetTimer(TIMER_CLOSEWND, 5000, nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineUnauthorize()
|
||||
{
|
||||
|
||||
@@ -230,6 +230,10 @@ public:
|
||||
VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength);
|
||||
// 显示用户上线信息
|
||||
CWnd* m_pFloatingTip = nullptr;
|
||||
// 屏幕预览:m_pFloatingTip 实际是 CPreviewTipWnd 时这里有同一指针的有类型副本,
|
||||
// 用于在收到 JPEG 后调用 SetImageFromJpeg;DeletePopupWindow 释放时一并置空。
|
||||
class CPreviewTipWnd* m_pPreviewTip = nullptr;
|
||||
WORD m_PreviewReqId = 0; // 当前期待的预览响应序号;0 = 无待响应
|
||||
// 记录 clientID(心跳更新)
|
||||
std::set<uint64_t> m_DirtyClients;
|
||||
// 待处理的上线/下线事件(批量更新减少闪烁)
|
||||
@@ -245,6 +249,12 @@ public:
|
||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||
void RebuildFilteredIndices(); // 重建过滤索引
|
||||
context* GetContextByListIndex(int iItem); // 根据列表索引获取 context(考虑分组过滤)
|
||||
|
||||
// 启发式 ID 迁移:新客户端首次上线时,按 (ComputerName, ProgramPath) 在 m_ClientMap
|
||||
// 里找老条目,若唯一匹配则把元数据(备注/位置/级别/授权)拷贝到 newId。
|
||||
// 多于一个候选时保守跳过,写日志让运维手动处理,避免误聚合。
|
||||
// 返回 true 表示有迁移发生(调用方需触发 dat 文件落盘)。
|
||||
bool TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath);
|
||||
void LoadListData(const std::string& group);
|
||||
void DeletePopupWindow(BOOL bForce = FALSE);
|
||||
void CheckHeartbeat();
|
||||
@@ -428,6 +438,12 @@ public:
|
||||
afx_msg void OnWhatIsThis();
|
||||
afx_msg void OnOnlineAuthorize();
|
||||
void OnListClick(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
// 屏幕预览:依 ctx 最近 RTT + 屏幕分辨率挑参数;4K/超宽屏在 LAN 档自适应放大
|
||||
void ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const;
|
||||
// 发起预览请求;reqId 应与 m_PreviewReqId 同步
|
||||
void SendScreenPreviewRequest(context* ctx, WORD reqId, WORD maxWidth, BYTE jpegQuality);
|
||||
// 收到 TOKEN_SCREEN_PREVIEW_RSP(在主线程处理)
|
||||
afx_msg LRESULT OnPreviewResponse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineUnauthorize();
|
||||
afx_msg void OnToolRequestAuth();
|
||||
afx_msg LRESULT OnPasswordCheck(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
<ClInclude Include="proxy\HPSocket.h" />
|
||||
<ClInclude Include="proxy\HPTypeDef.h" />
|
||||
@@ -388,6 +389,7 @@
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="..\..\client\MemoryModule.c">
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
<ClCompile Include="WebService.cpp" />
|
||||
<ClCompile Include="msvc_compat.c" />
|
||||
<ClCompile Include="PluginSettingsDlg.cpp" />
|
||||
<ClCompile Include="PreviewTipWnd.cpp" />
|
||||
<ClCompile Include="TriggerSettingsDlg.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -185,6 +186,7 @@
|
||||
<ClInclude Include="WebPage.h" />
|
||||
<ClInclude Include="SimpleWebSocket.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -30,17 +30,15 @@ CString CFileManagerDlg::s_strLocalHistoryPath;
|
||||
std::map<uint64_t, CString> CFileManagerDlg::s_mapRemoteHistoryPath;
|
||||
CLock CFileManagerDlg::s_lockHistory;
|
||||
|
||||
// 获取有效的客户端ID:优先用 m_ClientID,否则通过 IP 找主连接
|
||||
// 获取有效的客户端ID:基类已经覆盖 m_ClientID + ctx->GetClientID()(含 auth 后钉的值),
|
||||
// 这里仅在它们都拿不到时(老客户端没走 auth)通过 IP 反查主连接做兜底。
|
||||
uint64_t CFileManagerDlg::GetClientID() const
|
||||
{
|
||||
// 优先使用已设置的 m_ClientID(未来 TOKEN_CLIENTID 会设置这个)
|
||||
if (m_ClientID != 0) {
|
||||
return m_ClientID;
|
||||
}
|
||||
// 回退:通过 IP 找主连接获取 ClientID(线程安全)
|
||||
uint64_t id = CDialogBase::GetClientID();
|
||||
if (id != 0) return id;
|
||||
// 老客户端兜底:通过 IP 找主连接获取 ClientID(线程安全)
|
||||
if (g_2015RemoteDlg && m_ContextObject) {
|
||||
std::string peerIP = m_ContextObject->GetPeerName();
|
||||
return g_2015RemoteDlg->FindClientIDByIP(peerIP);
|
||||
return g_2015RemoteDlg->FindClientIDByIP(m_ContextObject->GetPeerName());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -229,7 +229,11 @@ public:
|
||||
return m_bIsClosed;
|
||||
}
|
||||
virtual uint64_t GetClientID() const {
|
||||
return m_ClientID;
|
||||
// 优先用 UpdateContext 设过的 m_ClientID(重连场景),否则取子连接 ctx 自身的 ID。
|
||||
// 子连接通过 TOKEN_CONN_AUTH 通过校验后,ctx->GetClientID() 已被钉成主连接的 clientID,
|
||||
// 这样 dialog 拿到的 ID 既准确又免去 IP 反查兜底(NAT/127.0.0.1 场景靠谱)。
|
||||
if (m_ClientID != 0) return m_ClientID;
|
||||
return m_ContextObject ? m_ContextObject->GetClientID() : 0;
|
||||
}
|
||||
BOOL SayByeBye()
|
||||
{
|
||||
|
||||
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
243
server/2015Remote/PreviewTipWnd.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
// PreviewTipWnd.cpp
|
||||
#include "stdafx.h"
|
||||
#include "PreviewTipWnd.h"
|
||||
|
||||
#include <objidl.h> // IUnknown / IStream — gdiplus.h 依赖它们已声明
|
||||
#include <gdiplus.h>
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
namespace {
|
||||
constexpr int PADDING = 8;
|
||||
constexpr int IMAGE_TEXT_GAP = 6; // 图像与下方文本之间的留白
|
||||
constexpr int MIN_TEXT_W = 360;
|
||||
constexpr int MAX_TEXT_W = 720; // 文本可换行宽度上限(垂直布局,宽度不再受图像挤压)
|
||||
constexpr int MAX_IMAGE_W = 960; // 显示上限(防止 LAN 档 1280 的 JPEG 把窗口顶到屏幕外)
|
||||
constexpr int LOADING_W = 480; // 占位的最小宽度(图像未到达时)
|
||||
constexpr int LOADING_H = 270;
|
||||
} // namespace
|
||||
|
||||
BEGIN_MESSAGE_MAP(CPreviewTipWnd, CWnd)
|
||||
ON_WM_PAINT()
|
||||
ON_WM_ERASEBKGND()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
CPreviewTipWnd::CPreviewTipWnd() = default;
|
||||
|
||||
CPreviewTipWnd::~CPreviewTipWnd()
|
||||
{
|
||||
// m_image 通过 unique_ptr 自动释放
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW)
|
||||
{
|
||||
m_text = text;
|
||||
m_imageReserveW = imageReserveW > 0 ? min(imageReserveW, MAX_IMAGE_W) : 0;
|
||||
m_imageReserveH = m_imageReserveW > 0 ? (m_imageReserveW * 9 / 16) : 0;
|
||||
if (m_imageReserveW > 0 && m_imageReserveW < LOADING_W) {
|
||||
m_imageReserveW = LOADING_W;
|
||||
m_imageReserveH = LOADING_H;
|
||||
}
|
||||
|
||||
// 创建字体(项目是 MBCS,但浮窗用宽字符;显式 LOGFONTW + W 版 API 避免编码混淆)
|
||||
LOGFONTW lf = {};
|
||||
HFONT hSysFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
|
||||
if (hSysFont && ::GetObjectW(hSysFont, sizeof(lf), &lf) == 0) {
|
||||
hSysFont = nullptr;
|
||||
}
|
||||
if (!hSysFont) {
|
||||
lf.lfHeight = -12;
|
||||
wcscpy_s(lf.lfFaceName, L"Microsoft YaHei");
|
||||
}
|
||||
HFONT hF = ::CreateFontIndirectW(&lf);
|
||||
if (hF) m_font.Attach(hF);
|
||||
|
||||
// 注册自绘窗口类:用 MFC 的 AfxRegisterWndClass 确保和 MFC 子类化机制兼容
|
||||
LPCTSTR kClass = AfxRegisterWndClass(
|
||||
CS_SAVEBITS,
|
||||
::LoadCursor(NULL, IDC_ARROW),
|
||||
(HBRUSH)(COLOR_INFOBK + 1),
|
||||
NULL);
|
||||
|
||||
// 临时尺寸;RecalcLayoutAndResize 会在创建后调整
|
||||
CRect rc(anchor.x, anchor.y, anchor.x + 400, anchor.y + 200);
|
||||
BOOL bOk = CWnd::CreateEx(
|
||||
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
kClass, _T(""),
|
||||
WS_POPUP | WS_BORDER,
|
||||
rc, pParent, 0);
|
||||
if (!bOk) return FALSE;
|
||||
|
||||
RecalcLayoutAndResize();
|
||||
|
||||
ShowWindow(SW_SHOWNOACTIVATE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::SetImageFromJpeg(const BYTE* data, size_t bytes)
|
||||
{
|
||||
if (!data || bytes == 0) return;
|
||||
|
||||
HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!hMem) return;
|
||||
void* p = ::GlobalLock(hMem);
|
||||
if (!p) { ::GlobalFree(hMem); return; }
|
||||
memcpy(p, data, bytes);
|
||||
::GlobalUnlock(hMem);
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(::CreateStreamOnHGlobal(hMem, TRUE, &stream))) {
|
||||
::GlobalFree(hMem);
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<Bitmap> bmp(new Bitmap(stream, FALSE));
|
||||
stream->Release(); // CreateStreamOnHGlobal(..., TRUE) 释放 stream 时会释放 hMem
|
||||
|
||||
if (!bmp || bmp->GetLastStatus() != Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = std::move(bmp);
|
||||
m_hasImage = true;
|
||||
m_unavailable = false;
|
||||
RecalcLayoutAndResize();
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::MarkPreviewUnavailable()
|
||||
{
|
||||
if (m_hasImage) return; // 已经有图就不再覆盖
|
||||
m_unavailable = true;
|
||||
if (GetSafeHwnd()) Invalidate();
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::RecalcLayoutAndResize()
|
||||
{
|
||||
HWND hWnd = GetSafeHwnd();
|
||||
if (!hWnd) return;
|
||||
|
||||
// 估算图像尺寸(先算图像,因为文本宽度要参考图像宽度对齐)
|
||||
if (m_image) {
|
||||
int iw = (int)m_image->GetWidth();
|
||||
int ih = (int)m_image->GetHeight();
|
||||
if (iw > MAX_IMAGE_W) {
|
||||
ih = (int)((double)ih * MAX_IMAGE_W / iw + 0.5);
|
||||
iw = MAX_IMAGE_W;
|
||||
}
|
||||
m_imageDrawW = iw;
|
||||
m_imageDrawH = ih;
|
||||
} else {
|
||||
m_imageDrawW = m_imageReserveW;
|
||||
m_imageDrawH = m_imageReserveH;
|
||||
}
|
||||
|
||||
// 文本换行宽度:与图像同宽(让文本视觉上对齐到图像下方),但不超过 MAX_TEXT_W
|
||||
// 没有图像时退化到 MAX_TEXT_W
|
||||
int textWrapW = m_imageDrawW > 0 ? min((int)MAX_TEXT_W, m_imageDrawW) : (int)MAX_TEXT_W;
|
||||
if (textWrapW < MIN_TEXT_W) textWrapW = MIN_TEXT_W;
|
||||
|
||||
// 估算文本尺寸(项目是 MBCS,但浮窗文本是宽字符,必须显式调用 W 版本)
|
||||
CClientDC dc(this);
|
||||
CFont* old = dc.SelectObject(&m_font);
|
||||
CRect rcText(0, 0, textWrapW, 32767);
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &rcText,
|
||||
DT_CALCRECT | DT_LEFT | DT_WORDBREAK | DT_NOPREFIX);
|
||||
m_textW = max((int)MIN_TEXT_W, rcText.Width());
|
||||
if (m_imageDrawW > 0) m_textW = max(m_textW, m_imageDrawW); // 至少与图像同宽
|
||||
m_textH = rcText.Height();
|
||||
dc.SelectObject(old);
|
||||
|
||||
int contentW = max(m_imageDrawW, m_textW);
|
||||
int gap = (m_imageDrawW > 0 && m_textH > 0) ? IMAGE_TEXT_GAP : 0;
|
||||
int totalW = PADDING + contentW + PADDING;
|
||||
int totalH = PADDING + m_imageDrawH + gap + m_textH + PADDING;
|
||||
|
||||
// 当前左上角
|
||||
CRect rc;
|
||||
GetWindowRect(&rc);
|
||||
int newX = rc.left;
|
||||
int newY = rc.top;
|
||||
|
||||
// 防止越出屏幕:右下夹紧到工作区
|
||||
HMONITOR hMon = MonitorFromPoint(CPoint(newX, newY), MONITOR_DEFAULTTONEAREST);
|
||||
MONITORINFO mi = { sizeof(mi) };
|
||||
if (hMon && GetMonitorInfo(hMon, &mi)) {
|
||||
if (newX + totalW > mi.rcWork.right) newX = max((int)mi.rcWork.left, mi.rcWork.right - totalW);
|
||||
if (newY + totalH > mi.rcWork.bottom) newY = max((int)mi.rcWork.top, mi.rcWork.bottom - totalH);
|
||||
}
|
||||
|
||||
SetWindowPos(NULL, newX, newY, totalW, totalH, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
|
||||
BOOL CPreviewTipWnd::OnEraseBkgnd(CDC* /*pDC*/)
|
||||
{
|
||||
return TRUE; // 在 OnPaint 里整体填充
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::OnPaint()
|
||||
{
|
||||
CPaintDC pdc(this);
|
||||
CRect rcClient;
|
||||
GetClientRect(&rcClient);
|
||||
|
||||
// 双缓冲
|
||||
CDC memDC;
|
||||
memDC.CreateCompatibleDC(&pdc);
|
||||
CBitmap memBmp;
|
||||
memBmp.CreateCompatibleBitmap(&pdc, rcClient.Width(), rcClient.Height());
|
||||
CBitmap* oldBmp = memDC.SelectObject(&memBmp);
|
||||
|
||||
memDC.FillSolidRect(&rcClient, ::GetSysColor(COLOR_INFOBK));
|
||||
|
||||
CFont* oldFont = memDC.SelectObject(&m_font);
|
||||
memDC.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
memDC.SetBkMode(TRANSPARENT);
|
||||
|
||||
int curY = PADDING;
|
||||
if (m_imageDrawW > 0 && m_imageDrawH > 0) {
|
||||
CRect rcImg(PADDING, curY, PADDING + m_imageDrawW, curY + m_imageDrawH);
|
||||
DrawImageArea(memDC, rcImg);
|
||||
curY += m_imageDrawH + IMAGE_TEXT_GAP;
|
||||
}
|
||||
|
||||
CRect rcText(PADDING, curY, PADDING + m_textW, curY + m_textH);
|
||||
DrawTextArea(memDC, rcText);
|
||||
|
||||
memDC.SelectObject(oldFont);
|
||||
pdc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &memDC, 0, 0, SRCCOPY);
|
||||
memDC.SelectObject(oldBmp);
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawImageArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
// 边框
|
||||
dc.Draw3dRect(&rc, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
|
||||
|
||||
if (m_hasImage && m_image) {
|
||||
Graphics g(dc.GetSafeHdc());
|
||||
g.SetInterpolationMode(InterpolationModeHighQualityBicubic);
|
||||
g.SetSmoothingMode(SmoothingModeHighQuality);
|
||||
g.DrawImage(m_image.get(), rc.left + 1, rc.top + 1, rc.Width() - 2, rc.Height() - 2);
|
||||
} else {
|
||||
// 占位灰色背景
|
||||
CRect rcInner = rc;
|
||||
rcInner.DeflateRect(1, 1);
|
||||
dc.FillSolidRect(&rcInner, RGB(245, 245, 245));
|
||||
|
||||
const wchar_t* placeholder = m_unavailable ? L"Preview Unavailable" : L"Loading Preview ...";
|
||||
UINT fmt = DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX;
|
||||
dc.SetTextColor(m_unavailable ? RGB(160, 80, 80) : RGB(120, 120, 120));
|
||||
RECT rcInnerRaw = rcInner;
|
||||
::DrawTextW(dc.GetSafeHdc(), placeholder, -1, &rcInnerRaw, fmt);
|
||||
dc.SetTextColor(::GetSysColor(COLOR_INFOTEXT));
|
||||
}
|
||||
}
|
||||
|
||||
void CPreviewTipWnd::DrawTextArea(CDC& dc, const CRect& rc)
|
||||
{
|
||||
RECT r = rc;
|
||||
::DrawTextW(dc.GetSafeHdc(), m_text, m_text.GetLength(), &r,
|
||||
DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOPREFIX);
|
||||
}
|
||||
57
server/2015Remote/PreviewTipWnd.h
Normal file
57
server/2015Remote/PreviewTipWnd.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// PreviewTipWnd.h
|
||||
// 双击在线主机时弹出的浮窗:上方 JPEG 缩略图,下方主机信息文本。
|
||||
// 无图片时显示"加载预览…"占位,供 OnListClick 即时弹窗、收到响应后再 SetImage 重画。
|
||||
#pragma once
|
||||
|
||||
#include <afxwin.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Gdiplus { class Bitmap; }
|
||||
|
||||
class CPreviewTipWnd : public CWnd
|
||||
{
|
||||
public:
|
||||
CPreviewTipWnd();
|
||||
virtual ~CPreviewTipWnd();
|
||||
|
||||
// 创建浮窗。
|
||||
// anchor 屏幕坐标,浮窗左上角
|
||||
// text 下方显示的主机详情文本(宽字符,确保跨语言系统正确渲染)
|
||||
// imageReserveW 上方图像区域预留宽度(即将到来的预览最大宽度,仅作初始布局)
|
||||
// 为 0 表示不预留 — 与老 STATIC 路径行为一致(仅文本)
|
||||
BOOL Create(CWnd* pParent, CPoint anchor, const CStringW& text, int imageReserveW);
|
||||
|
||||
// 收到 JPEG 后调用:解码并重画。线程安全前提是只在主 UI 线程调用。
|
||||
void SetImageFromJpeg(const BYTE* data, size_t bytes);
|
||||
// 标记预览不可用(请求超时 / 客户端报错)。
|
||||
void MarkPreviewUnavailable();
|
||||
|
||||
WORD GetReqId() const { return m_reqId; }
|
||||
void SetReqId(WORD id) { m_reqId = id; }
|
||||
|
||||
protected:
|
||||
afx_msg void OnPaint();
|
||||
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
void RecalcLayoutAndResize();
|
||||
void DrawImageArea(CDC& dc, const CRect& rc);
|
||||
void DrawTextArea(CDC& dc, const CRect& rc);
|
||||
|
||||
CStringW m_text;
|
||||
int m_imageReserveW = 0; // 预留图像宽度(图像未到达时占位)
|
||||
int m_imageReserveH = 0; // 预留图像高度(按 16:9)
|
||||
int m_imageDrawW = 0; // 实际图像绘制宽度
|
||||
int m_imageDrawH = 0; // 实际图像绘制高度
|
||||
int m_textW = 0;
|
||||
int m_textH = 0;
|
||||
|
||||
bool m_hasImage = false;
|
||||
bool m_unavailable = false;
|
||||
std::unique_ptr<Gdiplus::Bitmap> m_image;
|
||||
|
||||
CFont m_font;
|
||||
WORD m_reqId = 0;
|
||||
};
|
||||
@@ -378,6 +378,11 @@ public:
|
||||
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
||||
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
std::atomic<bool> m_bAuthenticated{false};
|
||||
|
||||
// 预分配的解压缩缓冲区,避免频繁内存分配
|
||||
PBYTE DecompressBuffer = nullptr;
|
||||
ULONG DecompressBufferSize = 0;
|
||||
@@ -510,7 +515,11 @@ public:
|
||||
// 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0
|
||||
IsRemoved.store(false, std::memory_order_release);
|
||||
IoRefCount.store(0, std::memory_order_release);
|
||||
// 复用对象池时清空校验状态
|
||||
m_bAuthenticated.store(false, std::memory_order_release);
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
uint64_t GetAliveTime()const
|
||||
{
|
||||
return time(0) - OnlineTime;
|
||||
|
||||
@@ -68,4 +68,11 @@ public:
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_UTF8) != 0;
|
||||
}
|
||||
|
||||
// 检查客户端是否支持屏幕预览(双击主机时拉缩略图)。
|
||||
bool SupportsScreenPreview() const {
|
||||
CString caps = GetClientData(ONLINELIST_CAPABILITIES);
|
||||
if (caps.IsEmpty()) return false;
|
||||
return (strtoul(caps.GetString(), nullptr, 16) & CLIENT_CAP_SCREEN_PREVIEW) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
#define WM_SHOWNOTIFY WM_USER+3031
|
||||
#define WM_DISCONNECT WM_USER+3032
|
||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
Reference in New Issue
Block a user