From 837d89c8b5cf1ad2aa6d99c607abbd950d54c458 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sat, 20 Jun 2026 12:30:43 +0200 Subject: [PATCH] Feat: Sub-license count limit - LicenseLimit field in licenses.ini + context menu - Add LicenseLimit field to LicenseInfo struct (0 = not set, unlimited) - Add GetLicenseLimit/SetLicenseLimit: read/write LicenseLimit key in licenses.ini - Append |lic:N to reserved field in TOKEN_AUTH response only when LicenseLimit > 0; absent |lic: means no limit (client defaults to 9999), so super admin authenticating to its own server is never falsely terminated - Add "Sub-license limit" item in CLicenseDlg right-click menu (1-9999, empty = clear limit); menu label shows current value in real time - Limit change takes effect when sub-client re-authenticates Co-Authored-By: Claude Sonnet 4.6 --- server/2015Remote/2015RemoteDlg.cpp | 9 ++- server/2015Remote/CLicenseDlg.cpp | 90 +++++++++++++++++++++++++++++ server/2015Remote/CLicenseDlg.h | 7 +++ server/2015Remote/lang/en_US.ini | 8 +++ server/2015Remote/lang/zh_TW.ini | 8 +++ server/2015Remote/resource.h | 3 +- 6 files changed, 122 insertions(+), 3 deletions(-) diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index f8f7f10..032d801 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -5391,15 +5391,20 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) offset += 1; // 空的 frpConfig } - // Reserved 字段:签名 "sig:" 防假服务器 - // 签名消息: "SN|valid(0/1)",客户端可用已知信息重建并验签 + // Reserved 字段:签名 + 下级数量上限 + // 格式: "sig:<88字符base64>"(无限制)或 "sig:<88字符base64>|lic:"(有限制) + // 签名消息: "SN|valid(0/1)";|lic:N 仅当 LicenseLimit 显式配置(>0)时才写入, + // 未配置(=0)则不写,客户端视为 9999(不限制)。超管自身无需配置,永不受限。 if (!m_v2KeyPath.empty() && len > 20) { std::string snForSig(szBuffer + 1, szBuffer + 20); while (!snForSig.empty() && snForSig.back() == '\0') snForSig.pop_back(); std::string signMsg = snForSig + "|" + (valid ? "1" : "0"); BYTE sig[V2_SIGNATURE_SIZE]; if (SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), sig)) { + int licLimit = GetLicenseLimit(snForSig); std::string reservedStr = "sig:" + base64Encode(sig, V2_SIGNATURE_SIZE); + if (licLimit > 0) + reservedStr += "|lic:" + std::to_string(licLimit); if (offset + reservedStr.size() + 1 < sizeof(resp)) { memcpy(resp + offset, reservedStr.c_str(), reservedStr.size() + 1); offset += reservedStr.size() + 1; diff --git a/server/2015Remote/CLicenseDlg.cpp b/server/2015Remote/CLicenseDlg.cpp index 0d5129d..56d61ad 100644 --- a/server/2015Remote/CLicenseDlg.cpp +++ b/server/2015Remote/CLicenseDlg.cpp @@ -46,6 +46,7 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx) ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete) ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp) ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp) + ON_COMMAND(ID_LICENSE_SET_LIMIT, &CLicenseDlg::OnLicenseSetLimit) END_MESSAGE_MAP() // 前向声明:实现位于本文件后段(Auto FRP 相关工具) @@ -110,6 +111,9 @@ std::vector GetAllLicenses(int* activeNum) it = kv.find("PendingQuota"); if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str()); + it = kv.find("LicenseLimit"); + if (it != kv.end()) info.LicenseLimit = atoi(it->second.c_str()); + licenses.push_back(info); } @@ -346,6 +350,15 @@ void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult) const auto& lic = m_Licenses[nIndex]; menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)...")); menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)...")); + { + CString strLimitLabel; + int curLimit = lic.LicenseLimit; + if (curLimit > 0) + strLimitLabel.FormatL("下级数量限制: %d (&L)...", curLimit); + else + strLimitLabel = _TR("下级数量限制 (&L)..."); + menu.AppendMenuL(MF_STRING, ID_LICENSE_SET_LIMIT, strLimitLabel); + } menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)...")); // 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP" @@ -1254,3 +1267,80 @@ void CLicenseDlg::OnLicenseRevokeFrp() lic.SerialNumber.c_str(), existingPort); MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION); } + +// 写入下级数量上限到 licenses.ini(limit <= 0 表示清除,恢复默认) +bool SetLicenseLimit(const std::string& deviceID, int limit) +{ + std::lock_guard _lock(LicensesIniMutex()); + std::string iniPath = GetLicensesPath(); + config cfg(iniPath); + if (cfg.GetStr(deviceID, "Passcode", "").empty()) + return false; + if (limit > 0) + cfg.SetInt(deviceID, "LicenseLimit", limit); + else + cfg.SetStr(deviceID, "LicenseLimit", ""); // 清除字段,恢复默认 10 + return true; +} + +// 读取下级数量上限;返回 0 表示未设置(调用方视为默认 10) +int GetLicenseLimit(const std::string& deviceID) +{ + std::lock_guard _lock(LicensesIniMutex()); + std::string iniPath = GetLicensesPath(); + config cfg(iniPath); + return cfg.GetInt(deviceID, "LicenseLimit", 0); +} + +void CLicenseDlg::OnLicenseSetLimit() +{ + int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED); + if (nItem < 0) + return; + + size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem); + if (nIndex >= m_Licenses.size()) + return; + + auto& lic = m_Licenses[nIndex]; + int curLimit = lic.LicenseLimit; + + CInputDialog dlg(this); + CString strTitle; + strTitle.FormatL("下级数量限制 (%s)", lic.SerialNumber.c_str()); + dlg.Init(strTitle, _L("数量上限(1-9999):")); + if (curLimit > 0) + dlg.m_str = std::to_string(curLimit).c_str(); + + if (dlg.DoModal() != IDOK) + return; + + std::string input(dlg.m_str); + // 去除首尾空格 + size_t s = input.find_first_not_of(" \t"); + if (s != std::string::npos) input = input.substr(s); + size_t e = input.find_last_not_of(" \t"); + if (e != std::string::npos) input = input.substr(0, e + 1); + + int newLimit = 0; + if (!input.empty()) { + newLimit = atoi(input.c_str()); + if (newLimit < 1 || newLimit > 9999) { + MessageBoxL("请输入 1 到 9999 之间的整数。", "输入无效", MB_ICONWARNING); + return; + } + } + + if (!SetLicenseLimit(lic.SerialNumber, newLimit)) + return; + + lic.LicenseLimit = newLimit; + CString confirm; + if (newLimit > 0) + confirm.FormatL("授权 %s 的下级数量上限已设置为 %d。\n下级重新认证后生效。", + lic.SerialNumber.c_str(), newLimit); + else + confirm.FormatL("授权 %s 的下级数量限制已清除。\n下级重新认证后生效。", + lic.SerialNumber.c_str()); + MessageBox(confirm, _TR("设置成功"), MB_OK | MB_ICONINFORMATION); +} diff --git a/server/2015Remote/CLicenseDlg.h b/server/2015Remote/CLicenseDlg.h index 88b28ec..0148315 100644 --- a/server/2015Remote/CLicenseDlg.h +++ b/server/2015Remote/CLicenseDlg.h @@ -27,6 +27,7 @@ struct LicenseInfo { std::string PendingExpireDate; // 预设的新过期日期(如 20270221,空表示无预设) int PendingHostNum = 0; // 预设的并发连接数 int PendingQuota = 0; // 预设的配额数量(支持多机器续期) + int LicenseLimit = 0; // 下级数量上限(0=未设置,下发时默认10) }; // 续期信息结构体 @@ -99,6 +100,7 @@ public: afx_msg void OnLicenseDelete(); afx_msg void OnLicenseAutoFrp(); afx_msg void OnLicenseRevokeFrp(); + afx_msg void OnLicenseSetLimit(); }; // 获取所有授权信息 @@ -130,5 +132,10 @@ int GetIPCountFromList(const std::string& ipListStr); std::string GetFirstIPFromList(const std::string& ipListStr); std::string FormatIPDisplay(const std::string& ipListStr); // 格式化显示: "[3] 192.168.1.1, ..." +// 下级数量限制(写 licenses.ini;读取见 GetLicenseLimit) +bool SetLicenseLimit(const std::string& deviceID, int limit); +// 读取下级数量上限;0 = 未设置,调用方应视为 10(默认值) +int GetLicenseLimit(const std::string& deviceID); + // 检查 IP+机器名 是否在授权数据库中存在 bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr); diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index aea1720..7f8d28f 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1935,3 +1935,11 @@ FRPC Զ [ȫʾ] Web!!!=[Security Warning] Please set web password!!! 鿴=View Window ôСȻԭٲ鿴=The window is minimized. Please restore it before viewing. +¼: %d (&L)...=License Limit: %d (&L)... +¼ (&L)...=License Limit (&L)... +¼ (%s)=License Limit (%s) +(1-9999):=Limit(1-9999): + 1 9999 ֮=Please input a integer range 1, 9999. +Ч=Invalid Input +Ȩ %s ¼Ϊ %d\n¼֤Ч=Sub-license limit for %s has been set to %d.\nTakes effect when the sub-client re-authenticates. +Ȩ %s ¼\n¼֤Ч=Sub-license limit for %s has been cleared.\nTakes effect when the sub-client re-authenticates. diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 4e852d9..a2c254b 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1926,3 +1926,11 @@ FRPC Զ [ȫʾ] Web!!!=[ȫʾ] Web!!! 鿴=鿴 ôСȻԭٲ鿴=ԓҕСՈ߀ԭٲ鿴 +¼: %d (&L)...=¼: %d (&L)... +¼ (&L)...=¼ (&L)... +¼ (%s)=¼ (%s) +(1-9999):=(1-9999): + 1 9999 ֮= 1 9999 ֮ +Ч=Ч +Ȩ %s ¼Ϊ %d\n¼֤Ч=Ȩ %s ¼Ϊ %d\n¼֤Ч +Ȩ %s ¼\n¼֤Ч=Ȩ %s ¼\n¼֤Ч diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 8e73ae7..4dd15ee 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -991,6 +991,7 @@ #define ID_PARAM_THUMBNAIL_PREVIEW 33050 #define ID_LICENSE_AUTO_FRP 33051 #define ID_LICENSE_REVOKE_FRP 33052 +#define ID_LICENSE_SET_LIMIT 33068 #define ID_Menu 33053 #define ID_33054 33054 #define ID_MENU_COMPRESS 33055 @@ -1013,7 +1014,7 @@ #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 391 -#define _APS_NEXT_COMMAND_VALUE 33068 +#define _APS_NEXT_COMMAND_VALUE 33069 #define _APS_NEXT_CONTROL_VALUE 2542 #define _APS_NEXT_SYMED_VALUE 105 #endif