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 <noreply@anthropic.com>
This commit is contained in:
@@ -5391,15 +5391,20 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
|||||||
offset += 1; // 空的 frpConfig
|
offset += 1; // 空的 frpConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserved 字段:签名 "sig:<base64(64字节ECDSA签名)>" 防假服务器
|
// Reserved 字段:签名 + 下级数量上限
|
||||||
// 签名消息: "SN|valid(0/1)",客户端可用已知信息重建并验签
|
// 格式: "sig:<88字符base64>"(无限制)或 "sig:<88字符base64>|lic:<N>"(有限制)
|
||||||
|
// 签名消息: "SN|valid(0/1)";|lic:N 仅当 LicenseLimit 显式配置(>0)时才写入,
|
||||||
|
// 未配置(=0)则不写,客户端视为 9999(不限制)。超管自身无需配置,永不受限。
|
||||||
if (!m_v2KeyPath.empty() && len > 20) {
|
if (!m_v2KeyPath.empty() && len > 20) {
|
||||||
std::string snForSig(szBuffer + 1, szBuffer + 20);
|
std::string snForSig(szBuffer + 1, szBuffer + 20);
|
||||||
while (!snForSig.empty() && snForSig.back() == '\0') snForSig.pop_back();
|
while (!snForSig.empty() && snForSig.back() == '\0') snForSig.pop_back();
|
||||||
std::string signMsg = snForSig + "|" + (valid ? "1" : "0");
|
std::string signMsg = snForSig + "|" + (valid ? "1" : "0");
|
||||||
BYTE sig[V2_SIGNATURE_SIZE];
|
BYTE sig[V2_SIGNATURE_SIZE];
|
||||||
if (SignMessageV2(m_v2KeyPath.c_str(), (const BYTE*)signMsg.c_str(), (int)signMsg.size(), sig)) {
|
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);
|
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)) {
|
if (offset + reservedStr.size() + 1 < sizeof(resp)) {
|
||||||
memcpy(resp + offset, reservedStr.c_str(), reservedStr.size() + 1);
|
memcpy(resp + offset, reservedStr.c_str(), reservedStr.size() + 1);
|
||||||
offset += reservedStr.size() + 1;
|
offset += reservedStr.size() + 1;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
|
|||||||
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_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
|
||||||
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
|
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
|
||||||
|
ON_COMMAND(ID_LICENSE_SET_LIMIT, &CLicenseDlg::OnLicenseSetLimit)
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
|
|
||||||
// 前向声明:实现位于本文件后段(Auto FRP 相关工具)
|
// 前向声明:实现位于本文件后段(Auto FRP 相关工具)
|
||||||
@@ -110,6 +111,9 @@ std::vector<LicenseInfo> GetAllLicenses(int* activeNum)
|
|||||||
it = kv.find("PendingQuota");
|
it = kv.find("PendingQuota");
|
||||||
if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str());
|
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);
|
licenses.push_back(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +350,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)..."));
|
||||||
|
{
|
||||||
|
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)..."));
|
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
|
||||||
|
|
||||||
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
|
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
|
||||||
@@ -1254,3 +1267,80 @@ void CLicenseDlg::OnLicenseRevokeFrp()
|
|||||||
lic.SerialNumber.c_str(), existingPort);
|
lic.SerialNumber.c_str(), existingPort);
|
||||||
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
|
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入下级数量上限到 licenses.ini(limit <= 0 表示清除,恢复默认)
|
||||||
|
bool SetLicenseLimit(const std::string& deviceID, int limit)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> _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<std::recursive_mutex> _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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct LicenseInfo {
|
|||||||
std::string PendingExpireDate; // 预设的新过期日期(如 20270221,空表示无预设)
|
std::string PendingExpireDate; // 预设的新过期日期(如 20270221,空表示无预设)
|
||||||
int PendingHostNum = 0; // 预设的并发连接数
|
int PendingHostNum = 0; // 预设的并发连接数
|
||||||
int PendingQuota = 0; // 预设的配额数量(支持多机器续期)
|
int PendingQuota = 0; // 预设的配额数量(支持多机器续期)
|
||||||
|
int LicenseLimit = 0; // 下级数量上限(0=未设置,下发时默认10)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 续期信息结构体
|
// 续期信息结构体
|
||||||
@@ -99,6 +100,7 @@ public:
|
|||||||
afx_msg void OnLicenseDelete();
|
afx_msg void OnLicenseDelete();
|
||||||
afx_msg void OnLicenseAutoFrp();
|
afx_msg void OnLicenseAutoFrp();
|
||||||
afx_msg void OnLicenseRevokeFrp();
|
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 GetFirstIPFromList(const std::string& ipListStr);
|
||||||
std::string FormatIPDisplay(const std::string& ipListStr); // 格式化显示: "[3] 192.168.1.1, ..."
|
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+机器名 是否在授权数据库中存在
|
// 检查 IP+机器名 是否在授权数据库中存在
|
||||||
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr);
|
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN = nullptr);
|
||||||
|
|||||||
@@ -1935,3 +1935,11 @@ FRPC Զ
|
|||||||
[安全提示] 请设置Web访问密码!!!=[Security Warning] Please set web password!!!
|
[安全提示] 请设置Web访问密码!!!=[Security Warning] Please set web password!!!
|
||||||
查看=View Window
|
查看=View Window
|
||||||
该窗口已最小化,请先还原后再查看。=The window is minimized. Please restore it before viewing.
|
该窗口已最小化,请先还原后再查看。=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.
|
||||||
|
|||||||
@@ -1926,3 +1926,11 @@ FRPC Զ
|
|||||||
[安全提示] 请设置Web访问密码!!!=[安全提示] 请设置Web访问密码!!!
|
[安全提示] 请设置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下级重新认证后生效。
|
||||||
|
|||||||
@@ -991,6 +991,7 @@
|
|||||||
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
|
#define ID_PARAM_THUMBNAIL_PREVIEW 33050
|
||||||
#define ID_LICENSE_AUTO_FRP 33051
|
#define ID_LICENSE_AUTO_FRP 33051
|
||||||
#define ID_LICENSE_REVOKE_FRP 33052
|
#define ID_LICENSE_REVOKE_FRP 33052
|
||||||
|
#define ID_LICENSE_SET_LIMIT 33068
|
||||||
#define ID_Menu 33053
|
#define ID_Menu 33053
|
||||||
#define ID_33054 33054
|
#define ID_33054 33054
|
||||||
#define ID_MENU_COMPRESS 33055
|
#define ID_MENU_COMPRESS 33055
|
||||||
@@ -1013,7 +1014,7 @@
|
|||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 391
|
#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_CONTROL_VALUE 2542
|
||||||
#define _APS_NEXT_SYMED_VALUE 105
|
#define _APS_NEXT_SYMED_VALUE 105
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user