Feature: Automatically start frp client for subordinate #2

Merged
yuanyuanxiang merged 3 commits from feature/auto_frp into main 2026-05-22 16:52:39 +00:00
10 changed files with 564 additions and 30 deletions

View File

@@ -90,6 +90,7 @@
#define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时4 秒未收到则提示"预览不可用" #define TIMER_PREVIEW_ARRIVAL 8 // 屏幕预览到达超时4 秒未收到则提示"预览不可用"
#define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定) #define TIMER_PREVIEW_LOOP 9 // "播放快照"循环拉取(间隔由 LOOP_INTERVAL_MS 决定)
#define TIMER_THUMBNAIL_REFRESH 10 // 主机列表缩略图后台刷新(间隔取自 m_ThumbnailCfg #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 TODO_NOTICE MessageBoxL("This feature has not been implemented!\nPlease contact: 962914132@qq.com", "提示", MB_ICONINFORMATION);
#define TINY_DLL_NAME "TinyRun.dll" #define TINY_DLL_NAME "TinyRun.dll"
#define FRPC_DLL_NAME "Frpc.dll" #define FRPC_DLL_NAME "Frpc.dll"
@@ -2186,6 +2187,9 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
#endif #endif
InitFrpClients(); InitFrpClients();
InitFrpcAuto(); // FRP 自动代理(由上级提供配置) 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, "正在启动网络服务..."); UPDATE_SPLASH(90, "正在启动网络服务...");
// 最后启动SOCKET // 最后启动SOCKET
@@ -2646,8 +2650,11 @@ bool CMy2015RemoteDlg::GetEffectiveMasterAddress(std::string& outIP, int& outPor
return false; // 使用本地配置 return false; // 使用本地配置
} }
// 日期字符串转 Unix 时间戳(当天 23:59:59 // 日期字符串转 Unix 时间戳(当天 23:59:59 UTC
// 输入: "20260323" -> 输出: 1774329599 (2026-03-23 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) static time_t DateToTimestamp(const std::string& dateStr)
{ {
if (dateStr.length() != 8) return 0; if (dateStr.length() != 8) return 0;
@@ -2657,7 +2664,7 @@ static time_t DateToTimestamp(const std::string& dateStr)
t.tm_mon = std::stoi(dateStr.substr(4, 2)) - 1; t.tm_mon = std::stoi(dateStr.substr(4, 2)) - 1;
t.tm_mday = std::stoi(dateStr.substr(6, 2)); t.tm_mday = std::stoi(dateStr.substr(6, 2));
t.tm_hour = 23; t.tm_min = 59; t.tm_sec = 59; t.tm_hour = 23; t.tm_min = 59; t.tm_sec = 59;
return mktime(&t); return _mkgmtime(&t);
} catch (...) { } catch (...) {
return 0; return 0;
} }
@@ -2804,6 +2811,14 @@ void CMy2015RemoteDlg::StartFrpcAuto(const FrpAutoConfig& cfg)
THIS_CFG.SetStr("frp_auto", "privilegeKey", cfg.privilegeKey); THIS_CFG.SetStr("frp_auto", "privilegeKey", cfg.privilegeKey);
THIS_CFG.SetStr("frp_auto", "expireDate", cfg.expireDate); 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_frpAutoStatus = STATUS_UNKNOWN;
m_hFrpAutoThread = CreateThread(NULL, 0, FrpcAutoThreadProc, this, 0, NULL); m_hFrpAutoThread = CreateThread(NULL, 0, FrpcAutoThreadProc, this, 0, NULL);
@@ -2885,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 + StartFrpcAutoStartFrpcAuto 会重写 [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() void CMy2015RemoteDlg::ApplyFrpSettings()
@@ -3250,6 +3331,9 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
if (nIDEvent == TIMER_STATUSBAR_UPDATE) { if (nIDEvent == TIMER_STATUSBAR_UPDATE) {
UpdateStatusBarStats(); UpdateStatusBarStats();
} }
if (nIDEvent == TIMER_FRP_CONFIG_CHECK) {
CheckUpperFrpConfigChange();
}
if (nIDEvent == TIMER_STATUSBAR_INIT) { if (nIDEvent == TIMER_STATUSBAR_INIT) {
KillTimer(TIMER_STATUSBAR_INIT); // 只执行一次 KillTimer(TIMER_STATUSBAR_INIT); // 只执行一次
// 强制重新计算状态栏分区宽度 // 强制重新计算状态栏分区宽度
@@ -3544,6 +3628,7 @@ void CMy2015RemoteDlg::Release()
#ifdef _WIN64 #ifdef _WIN64
StopLocalFrpsServer(); // 停止本地 FRPS 服务器 StopLocalFrpsServer(); // 停止本地 FRPS 服务器
#endif #endif
KillTimer(TIMER_FRP_CONFIG_CHECK); // 必须先于 StopFrpcAuto避免末班定时器再次起飞 FRPC
StopFrpcAuto(); // 停止 FRP 自动代理 StopFrpcAuto(); // 停止 FRP 自动代理
THIS_APP->Destroy(); THIS_APP->Destroy();

View File

@@ -224,6 +224,10 @@ public:
Buffer* m_ServerBin[PAYLOAD_MAXTYPE]; Buffer* m_ServerBin[PAYLOAD_MAXTYPE];
Buffer* m_TinyRun[PAYLOAD_MAXTYPE] = {}; Buffer* m_TinyRun[PAYLOAD_MAXTYPE] = {};
MasterSettings m_settings; MasterSettings m_settings;
// 缓存上次检测到的上级 FRP 配置([settings] FrpConfig由定时器检测外部模块写入的变更。
// 检出变更后会热切换 FRPC首次=启动 / 覆盖=重启 / 撤销=停止),并在主对话框信息列表中给出友好提示。
std::string m_lastSeenFrpConfig;
void CheckUpperFrpConfigChange();
static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject); static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject);
static BOOL CALLBACK OfflineProc(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); int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr);

View File

@@ -10,6 +10,8 @@
#include "2015RemoteDlg.h" #include "2015RemoteDlg.h"
#include "InputDlg.h" #include "InputDlg.h"
#include "IPHistoryDlg.h" #include "IPHistoryDlg.h"
#include "FrpsForSubDlg.h"
#include "pwd_gen.h"
#include <algorithm> #include <algorithm>
// CLicenseDlg 对话框 // CLicenseDlg 对话框
@@ -42,14 +44,24 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark) ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs) ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete) 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() 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::vector<LicenseInfo> GetAllLicenses()
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::vector<LicenseInfo> licenses; std::vector<LicenseInfo> licenses;
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
// 注意CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
// 写入到一半的中间态。
CIniParser parser; CIniParser parser;
if (!parser.LoadFile(iniPath.c_str())) if (!parser.LoadFile(iniPath.c_str()))
return licenses; 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) bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -331,6 +344,15 @@ void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
const auto& lic = m_Licenses[nIndex]; const auto& lic = m_Licenses[nIndex];
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)...")); 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_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 历史选项 // 只有当有 IP 记录时才显示查看 IP 历史选项
int ipCount = GetIPCountFromList(lic.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) 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(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -458,6 +481,7 @@ bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDat
// 获取待续期信息 // 获取待续期信息
RenewalInfo GetPendingRenewal(const std::string& deviceID) RenewalInfo GetPendingRenewal(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
RenewalInfo info; RenewalInfo info;
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -471,6 +495,7 @@ RenewalInfo GetPendingRenewal(const std::string& deviceID)
// 清除待续期信息 // 清除待续期信息
bool ClearPendingRenewal(const std::string& deviceID) bool ClearPendingRenewal(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); 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) bool DecrementPendingQuota(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -495,7 +523,7 @@ bool DecrementPendingQuota(const std::string& deviceID)
cfg.SetInt(deviceID, "PendingQuota", quota); cfg.SetInt(deviceID, "PendingQuota", quota);
if (quota <= 0) { if (quota <= 0) {
// 配额用完,清除待续期信息 // 配额用完,清除待续期信息嵌套加锁recursive_mutex 安全)
ClearPendingRenewal(deviceID); ClearPendingRenewal(deviceID);
return false; return false;
} }
@@ -599,6 +627,7 @@ void CLicenseDlg::OnLicenseRenewal()
// 设置授权备注 // 设置授权备注
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark) bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -642,6 +671,7 @@ void CLicenseDlg::OnLicenseEditRemark()
// 删除授权 // 删除授权
bool DeleteLicense(const std::string& deviceID) bool DeleteLicense(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -651,9 +681,21 @@ bool DeleteLicense(const std::string& deviceID)
return false; // 授权不存在 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) // 删除该 section (通过写入 NULL 删除整个 section)
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str()); BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存 ::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
// 关键:清掉 UpdateLicenseActivity 的内存缓存。否则若同 SN 客户端再次连上来,
// cache 命中会跳过落盘 → disk 永远不会重建被删的 section。
InvalidateLicenseActivityCache(deviceID);
return ret != FALSE; return ret != FALSE;
} }
@@ -835,12 +877,17 @@ void CLicenseDlg::OnLicenseViewIPs()
// 如果有记录被删除,保存更新后的 IP 列表 // 如果有记录被删除,保存更新后的 IP 列表
if (removedCount > 0) { if (removedCount > 0) {
// 锁内只做 I/O —— UI 控件更新SetItemText放锁外避免锁内触发
// 任何可能的消息循环回调,保持锁占用时间最短
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
cfg.SetStr(lic.SerialNumber, "IP", newIPList); cfg.SetStr(lic.SerialNumber, "IP", newIPList);
lic.IP = newIPList; // 更新内存中的数据 }
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
// 更新列表显示 // 更新列表显示UI 线程操作,必须在锁外)
CString strIPDisplay = FormatIPDisplay(newIPList).c_str(); CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay); 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; if (ip.empty()) return false;
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
// GetAllLicenses 内部也加锁recursive_mutex 允许嵌套。
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
auto licenses = GetAllLicenses(); auto licenses = GetAllLicenses();
for (const auto& lic : licenses) { for (const auto& lic : licenses) {
if (lic.IP.empty()) continue; if (lic.IP.empty()) continue;
@@ -1010,3 +1060,195 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
} }
return false; 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, ""); // 写入空 ownerFindNextAvailablePort 将其视为可用
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);
}

View File

@@ -97,6 +97,8 @@ public:
afx_msg void OnLicenseEditRemark(); afx_msg void OnLicenseEditRemark();
afx_msg void OnLicenseViewIPs(); afx_msg void OnLicenseViewIPs();
afx_msg void OnLicenseDelete(); afx_msg void OnLicenseDelete();
afx_msg void OnLicenseAutoFrp();
afx_msg void OnLicenseRevokeFrp();
}; };
// 获取所有授权信息 // 获取所有授权信息

View File

@@ -14,11 +14,65 @@
#include "InputDlg.h" #include "InputDlg.h"
#include "FrpsForSubDlg.h" #include "FrpsForSubDlg.h"
#include "UIBranding.h" #include "UIBranding.h"
#include <unordered_map>
#include <ctime>
// 外部函数声明 // 外部函数声明
extern std::vector<std::string> splitString(const std::string& str, char delimiter); extern std::vector<std::string> splitString(const std::string& str, char delimiter);
extern std::string GetFirstMasterIP(const std::string& master); 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 对话框 // CPasswordDlg 对话框
IMPLEMENT_DYNAMIC(CPasswordDlg, CDialogEx) 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& authorization,
const std::string& frpConfig) const std::string& frpConfig)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); 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, bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
std::string& hmac, std::string& remark) std::string& hmac, std::string& remark)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -161,6 +217,7 @@ bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
// 加载授权的 FRP 配置 // 加载授权的 FRP 配置
std::string LoadLicenseFrpConfig(const std::string& deviceID) std::string LoadLicenseFrpConfig(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
return cfg.GetStr(deviceID, "FrpConfig", ""); return cfg.GetStr(deviceID, "FrpConfig", "");
@@ -169,6 +226,7 @@ std::string LoadLicenseFrpConfig(const std::string& deviceID)
// 加载授权的 Authorization用于 V2 授权返回给第一层) // 加载授权的 Authorization用于 V2 授权返回给第一层)
std::string LoadLicenseAuthorization(const std::string& deviceID) std::string LoadLicenseAuthorization(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
return cfg.GetStr(deviceID, "Authorization", ""); return cfg.GetStr(deviceID, "Authorization", "");
@@ -177,6 +235,7 @@ std::string LoadLicenseAuthorization(const std::string& deviceID)
// 更新授权的 AuthorizationV2 续期时更新) // 更新授权的 AuthorizationV2 续期时更新)
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization) bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
@@ -313,59 +372,115 @@ static int GetIPCount(const std::string& ipListStr)
return (int)ipList.size(); 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, bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
const std::string& hmac, const std::string& ip, const std::string& hmac, const std::string& ip,
const std::string& location, const std::string& machineName) 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 entryIP 字段会在不同客户端间反复
// 翻转,每次心跳都判定为 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(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
// 检查该授权是否存在 // 检查该授权是否存在(注意:此处仍需读磁盘,因为我们不缓存"是否存在"的事实)
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", ""); 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, "SerialNumber", deviceID);
cfg.SetStr(deviceID, "Passcode", passcode); cfg.SetStr(deviceID, "Passcode", passcode);
cfg.SetStr(deviceID, "HMAC", hmac); cfg.SetStr(deviceID, "HMAC", hmac);
cfg.SetStr(deviceID, "Remark", "既往授权自动加入"); cfg.SetStr(deviceID, "Remark", "既往授权自动加入");
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); // 新记录默认为有效 cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
} else { } else {
// 授权已存在,更新 passcode(续期后 passcode 会变化 // 已存在:只在 passcode/hmac 实际变化时才写(续期场景才会变
if (firstTime || passcodeChanged) {
cfg.SetStr(deviceID, "Passcode", passcode); cfg.SetStr(deviceID, "Passcode", passcode);
}
if (firstTime || hmacChanged) {
cfg.SetStr(deviceID, "HMAC", hmac); cfg.SetStr(deviceID, "HMAC", hmac);
} }
}
// 更新最后活跃时间 // LastActiveTime走到这里就更新节流过期或字段变化都需要刷新
SYSTEMTIME st;
GetLocalTime(&st);
char timeStr[32]; char timeStr[32];
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d", sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
cfg.SetStr(deviceID, "LastActiveTime", timeStr); cfg.SetStr(deviceID, "LastActiveTime", timeStr);
// 如果是新添加的记录,设置创建时间 if (isNewRecord) {
if (existingPasscode.empty()) {
cfg.SetStr(deviceID, "CreateTime", timeStr); cfg.SetStr(deviceID, "CreateTime", timeStr);
} }
// 更新 IP 列表(追加新 IP 或更新已有 IP 的时间戳 // IP 列表:本客户端首次 或 同客户端跨天 才重写UpdateIPList 会在 disk 上合并
// 格式: IP(机器名)|yyMMdd if (!ip.empty() && (firstTime || ipDayChanged)) {
if (!ip.empty()) {
std::string existingIPList = cfg.GetStr(deviceID, "IP", ""); std::string existingIPList = cfg.GetStr(deviceID, "IP", "");
std::string newIPList = UpdateIPList(existingIPList, ip, machineName); std::string newIPList = UpdateIPList(existingIPList, ip, machineName);
cfg.SetStr(deviceID, "IP", newIPList); cfg.SetStr(deviceID, "IP", newIPList);
cache.lastIPWriteDate = today;
} }
if (!location.empty()) {
if (!location.empty() && (firstTime || locationChanged)) {
cfg.SetStr(deviceID, "Location", location); cfg.SetStr(deviceID, "Location", location);
} }
// —— 同步缓存(必须在落盘成功后)——
cache.lastFlushTime = now;
cache.lastPasscode = passcode;
cache.lastHMAC = hmac;
if (!location.empty()) {
cache.lastLocation = location;
}
return true; return true;
} }
// 检查授权是否已被撤销 // 检查授权是否已被撤销
bool IsLicenseRevoked(const std::string& deviceID) bool IsLicenseRevoked(const std::string& deviceID)
{ {
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath(); std::string iniPath = GetLicensesPath();
config cfg(iniPath); config cfg(iniPath);
std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);

View File

@@ -2,10 +2,23 @@
#include <afx.h> #include <afx.h>
#include <afxwin.h> #include <afxwin.h>
#include <mutex>
#include "Resource.h" #include "Resource.h"
#include "common/commands.h" #include "common/commands.h"
#include "LangManager.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 对话框 // CPasswordDlg 对话框
namespace TcpClient { namespace TcpClient {
std::string ObfuscateAuthorization(const std::string& auth); std::string ObfuscateAuthorization(const std::string& auth);

View File

@@ -1854,3 +1854,37 @@ IOCP
入站公网 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. 检测到入站连接来自公网 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. 检测到可疑连接:内核 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

View File

@@ -1845,3 +1845,37 @@ IOCP
入站公网 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詳細記錄請見訊息列表與執行日誌。 检测到入站连接来自公网 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詳細記錄請見訊息列表與執行日誌。 检测到可疑连接:内核 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 已釋放。
撤销成功=撤銷成功

View File

@@ -710,7 +710,10 @@ bool IsFrpTokenEncoded(const std::string& privilegeKey)
return privilegeKey.length() >= 4 && privilegeKey.substr(0, 4) == "ENC:"; 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) time_t FrpDateToTimestamp(const std::string& dateStr)
{ {
if (dateStr.length() != 8) return 0; if (dateStr.length() != 8) return 0;
@@ -722,7 +725,7 @@ time_t FrpDateToTimestamp(const std::string& dateStr)
t.tm_hour = 23; t.tm_hour = 23;
t.tm_min = 59; t.tm_min = 59;
t.tm_sec = 59; t.tm_sec = 59;
return mktime(&t); return _mkgmtime(&t);
} catch (...) { } catch (...) {
return 0; return 0;
} }

View File

@@ -983,6 +983,8 @@
#define ID_33048 33048 #define ID_33048 33048
#define ID_SCREENPREVIEW_LOOP 33049 #define ID_SCREENPREVIEW_LOOP 33049
#define ID_PARAM_THUMBNAIL_PREVIEW 33050 #define ID_PARAM_THUMBNAIL_PREVIEW 33050
#define ID_LICENSE_AUTO_FRP 33051
#define ID_LICENSE_REVOKE_FRP 33052
#define ID_EXIT_FULLSCREEN 40001 #define ID_EXIT_FULLSCREEN 40001
// Next default values for new objects // Next default values for new objects
@@ -990,7 +992,7 @@
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 386 #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_CONTROL_VALUE 2542
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif