diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index cc71c98..b8ab523 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -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" @@ -2186,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 @@ -2646,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; @@ -2657,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; } @@ -2804,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); @@ -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 + 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() @@ -3250,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); // 只执行一次 // 强制重新计算状态栏分区宽度 @@ -3544,6 +3628,7 @@ void CMy2015RemoteDlg::Release() #ifdef _WIN64 StopLocalFrpsServer(); // 停止本地 FRPS 服务器 #endif + KillTimer(TIMER_FRP_CONFIG_CHECK); // 必须先于 StopFrpcAuto,避免末班定时器再次起飞 FRPC StopFrpcAuto(); // 停止 FRP 自动代理 THIS_APP->Destroy(); diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index ae5442c..e0a21c7 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -224,6 +224,10 @@ public: 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); diff --git a/server/2015Remote/CLicenseDlg.cpp b/server/2015Remote/CLicenseDlg.cpp index 71224df..f3b0e77 100644 --- a/server/2015Remote/CLicenseDlg.cpp +++ b/server/2015Remote/CLicenseDlg.cpp @@ -10,6 +10,8 @@ #include "2015RemoteDlg.h" #include "InputDlg.h" #include "IPHistoryDlg.h" +#include "FrpsForSubDlg.h" +#include "pwd_gen.h" #include // 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 GetAllLicenses() { + std::lock_guard _lock(LicensesIniMutex()); std::vector 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 _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 _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 _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 _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 _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 _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 _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) { - std::string iniPath = GetLicensesPath(); - config cfg(iniPath); - cfg.SetStr(lic.SerialNumber, "IP", newIPList); - lic.IP = newIPList; // 更新内存中的数据 + // 锁内只做 I/O —— UI 控件更新(SetItemText)放锁外,避免锁内触发 + // 任何可能的消息循环回调,保持锁占用时间最短 + { + std::lock_guard _lock(LicensesIniMutex()); + std::string iniPath = GetLicensesPath(); + config cfg(iniPath); + cfg.SetStr(lic.SerialNumber, "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 _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 _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 _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); +} diff --git a/server/2015Remote/CLicenseDlg.h b/server/2015Remote/CLicenseDlg.h index e546d38..43ab6c2 100644 --- a/server/2015Remote/CLicenseDlg.h +++ b/server/2015Remote/CLicenseDlg.h @@ -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(); }; // 获取所有授权信息 diff --git a/server/2015Remote/CPasswordDlg.cpp b/server/2015Remote/CPasswordDlg.cpp index 93f2ec7..5217d01 100644 --- a/server/2015Remote/CPasswordDlg.cpp +++ b/server/2015Remote/CPasswordDlg.cpp @@ -14,11 +14,65 @@ #include "InputDlg.h" #include "FrpsForSubDlg.h" #include "UIBranding.h" +#include +#include // 外部函数声明 extern std::vector 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 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 _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 _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 _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 _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 _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 _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 _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 会变化) - cfg.SetStr(deviceID, "Passcode", passcode); - cfg.SetStr(deviceID, "HMAC", hmac); + // 已存在:只在 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 _lock(LicensesIniMutex()); std::string iniPath = GetLicensesPath(); config cfg(iniPath); std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); diff --git a/server/2015Remote/CPasswordDlg.h b/server/2015Remote/CPasswordDlg.h index abcb4f8..ada2fd8 100644 --- a/server/2015Remote/CPasswordDlg.h +++ b/server/2015Remote/CPasswordDlg.h @@ -2,10 +2,23 @@ #include #include +#include #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); diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index e047303..13f22f2 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -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\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Ƿ񸲸Dzã=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 diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index c2f895e..bdeae55 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1845,3 +1845,37 @@ IOCP վ IP=%s Proxy Protocol ʵ IP raw TCP Զˣ=վW IP=%s Proxy Protocol 挍 IP raw TCP ˣ ⵽վԹ IP%s\r\n\r\nð LAN ãʹΥȨ\r\nԶأзʽȨ\r\n\r\nϸ¼Ϣб־=zyվBԹW IP%s\r\n\r\nԇðH LAN ãWʹÌ`ڙl\r\nWhأՈlзՈʽڙࡣ\r\n\r\nԔӛՈҊӍϢбcI ⵽ӣں RTT λ %d msֵ %d ms\r\n\r\nƫߵ RTT ʾӿܾɴ / VPN / ת\r\nð LAN ãʹΥȨ\r\n\r\nԶأзʽȨ\r\nϸ¼Ϣб־=zyB RTT λ %d msֵ %d ms\r\n\r\nmƫߵ RTT ʾԓBܽɴ / VPN / D\r\nԇðH LAN ãWʹÌ`ڙl\r\n\r\nWhأՈlзՈʽڙࡣ\r\nԔӛՈҊӍϢбcI +; 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 hBӲ %dЧ +[FRP] յЧ FRP : %s=[FRP] յoЧ FRP : %s +[FRP] ϼѳ FRP ãֹͣ FRPC=[FRP] ϼѳN FRP ãֹͣ FRPC +[FRP] ϼ FRP лԶ̶˿ %d %dЧ=[FRP] ϼ FRP ГQ(hBӲ %d %d)Ч +[FRP] յЧ FRP : %sֹͣ FRPC=[FRP] յoЧ FRP : %sֹͣf FRPC + +; --- ȨҼ˵"ԶFRP" --- +ԶFRP(&F)...=ԄFRP(&F)... + +; --- Ȩ ԶFRP Ի --- + չ ¼ FRP ò FRPS ַ˿ TokenȻʹô˹ܡ=Ո U ¼ FRP O ІÁKO FRPS ŷλַBӲc TokenȻʹô˹ܡ +δ FRPS=δO FRPS +Ȩѷ FRPC Զ̶˿ %dǷ񸲸Dzã=ԓڙѷ FRPC hBӲ %dǷwKO + FRPC =w FRPC O +FRPS ˿ڷΧ޷Զ䣬¼ FRP áеĶ˿ڷΧ=FRPS BӲѝMoԄӷ䣬ՈU¼ FRP OеBӲ +˿=BӲѝM +Զ FRP - %s=Ԅ FRP - %s +FRPC Զ̶˿ (1024-65535):=FRPC hBӲ (1024-65535): +˿ %d ѱк %s ռãѡ˿ڡ=BӲ %d ѱ̖ %s ãՈxBӲ +˿ڳͻ=BӲnͻ + FRP ʧ=a FRP ʧ +ΪȨ %s FRPC Զ̶˿ %d\n\n¼ʱԶ FRP ò÷=ўڙ %s O FRPC hBӲ %d\n\n¼ϾrԄӽ FRP ÁK÷ +óɹ=Oɹ +; --- ȨҼ˵"FRP" + Ի --- +FRP(&U)=NFRP(&U) +ȷȨ %s FRP \n\nԶ̶˿ %d ͷţ¼´ߺʧЧ=_Nڙ %s FRP Æ᣿\n\nhBӲ %d ጷţ¼´ϾᷴʧЧ + FRP =N FRP +ѳȨ %s FRP ãԶ̶˿ %d ͷš=ѳNڙ %s FRP ãhBӲ %d ጷš +ɹ=Nɹ diff --git a/server/2015Remote/pwd_gen.cpp b/server/2015Remote/pwd_gen.cpp index 827ae54..842d288 100644 --- a/server/2015Remote/pwd_gen.cpp +++ b/server/2015Remote/pwd_gen.cpp @@ -710,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; @@ -722,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; } diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 821ec30..f93e873 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -983,14 +983,16 @@ #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 -// +// #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