licenses.ini was hit on every heartbeat -- 5s x clients x ~8 SetStr per
auth -- with no concurrency protection. Two consequences:
1. 100 concurrent online would saturate the file (~160 writes/sec,
full-file rewrite each via WritePrivateProfileString).
2. Concurrent SetPendingRenewal / DecrementPendingQuota with no lock
occasionally clobbered freshly-set renewal quotas (reported by
user as "preset renewal silently disappears").
Add LicensesIniMutex() (Meyers singleton recursive_mutex, exposed in
CPasswordDlg.h so both CPasswordDlg.cpp and CLicenseDlg.cpp share it)
and wrap all 15 functions that touch licenses.ini.
Rewrite UpdateLicenseActivity around g_activityCache (in-memory state
keyed by "SN|IP|machine"): skip the entire write path when nothing
changed and the 30s LastActiveTime throttle window hasn't expired.
Passcode/HMAC are only flushed on actual change (renewal path); IP
list is only rewritten when the yyMMdd timestamp would roll a day.
Measured impact (local 2-client baseline):
before: 0.60 writes/sec (4 writes per heartbeat cluster)
after: 0.07 writes/sec (one write per client per 30s throttle)
Extrapolated to the 100-online target:
before: ~160 writes/sec (saturation)
after: ~3.3 writes/sec (100 clients / 30s throttle window)
Race elimination is the more important win: PendingQuota's
read-modify-write is now atomic, so the "preset renewal disappears"
race is closed.
Notes from audit (these landed during the same iteration):
- Cache key is (SN, IP, machine), not SN alone. A single SN can be
shared by 100+ end machines in bulk-license deployments, so a
per-SN cache flips on every heartbeat and defeats suppression.
Per-(SN, IP, machine) throttling is what makes the 100/30 model
actually hold; an SN-only key reproduced the original ~0.7 writes/s.
- DeleteLicense invalidates the per-SN activity cache via
InvalidateLicenseActivityCache() (prefix scan since one SN maps to
many cache entries). Without this, cache hits after delete would
skip the auto-recreate path and leave the section permanently
missing.
- OnLicenseViewIPs: m_ListLicense.SetItemText moved outside the lock
so the critical section only covers disk I/O.
1255 lines
43 KiB
C++
1255 lines
43 KiB
C++
// CLicenseDlg.cpp: 实现文件
|
||
//
|
||
|
||
#include "stdafx.h"
|
||
#include "CLicenseDlg.h"
|
||
#include "afxdialogex.h"
|
||
#include "CPasswordDlg.h"
|
||
#include "common/iniFile.h"
|
||
#include "common/IniParser.h"
|
||
#include "2015RemoteDlg.h"
|
||
#include "InputDlg.h"
|
||
#include "IPHistoryDlg.h"
|
||
#include "FrpsForSubDlg.h"
|
||
#include "pwd_gen.h"
|
||
#include <algorithm>
|
||
|
||
// CLicenseDlg 对话框
|
||
|
||
IMPLEMENT_DYNAMIC(CLicenseDlg, CDialogEx)
|
||
|
||
CLicenseDlg::CLicenseDlg(CMy2015RemoteDlg* pParent /*=nullptr*/)
|
||
: CDialogLangEx(IDD_DIALOG_LICENSE, pParent)
|
||
, m_pParent(pParent)
|
||
{
|
||
}
|
||
|
||
CLicenseDlg::~CLicenseDlg()
|
||
{
|
||
}
|
||
|
||
void CLicenseDlg::DoDataExchange(CDataExchange* pDX)
|
||
{
|
||
__super::DoDataExchange(pDX);
|
||
DDX_Control(pDX, IDC_LICENSE_LIST, m_ListLicense);
|
||
}
|
||
|
||
BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
|
||
ON_WM_SIZE()
|
||
ON_NOTIFY(NM_RCLICK, IDC_LICENSE_LIST, &CLicenseDlg::OnNMRClickLicenseList)
|
||
ON_NOTIFY(LVN_COLUMNCLICK, IDC_LICENSE_LIST, &CLicenseDlg::OnColumnClick)
|
||
ON_COMMAND(ID_LICENSE_REVOKE, &CLicenseDlg::OnLicenseRevoke)
|
||
ON_COMMAND(ID_LICENSE_ACTIVATE, &CLicenseDlg::OnLicenseActivate)
|
||
ON_COMMAND(ID_LICENSE_RENEWAL, &CLicenseDlg::OnLicenseRenewal)
|
||
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
|
||
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
|
||
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
|
||
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
|
||
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
|
||
END_MESSAGE_MAP()
|
||
|
||
// 前向声明:实现位于本文件后段(Auto FRP 相关工具)
|
||
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig);
|
||
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
||
|
||
// 获取所有授权信息
|
||
std::vector<LicenseInfo> GetAllLicenses()
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::vector<LicenseInfo> licenses;
|
||
std::string iniPath = GetLicensesPath();
|
||
|
||
// 注意:CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
|
||
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
|
||
// 写入到一半的中间态。
|
||
CIniParser parser;
|
||
if (!parser.LoadFile(iniPath.c_str()))
|
||
return licenses;
|
||
|
||
const auto& sections = parser.GetAllSections();
|
||
for (const auto& sec : sections) {
|
||
const std::string& sectionName = sec.first;
|
||
const auto& kv = sec.second;
|
||
|
||
LicenseInfo info;
|
||
info.SerialNumber = sectionName;
|
||
|
||
auto it = kv.find("Passcode");
|
||
if (it != kv.end()) info.Passcode = it->second;
|
||
|
||
it = kv.find("HMAC");
|
||
if (it != kv.end()) info.HMAC = it->second;
|
||
|
||
it = kv.find("Remark");
|
||
if (it != kv.end()) info.Remark = it->second;
|
||
|
||
it = kv.find("CreateTime");
|
||
if (it != kv.end()) info.CreateTime = it->second;
|
||
|
||
it = kv.find("IP");
|
||
if (it != kv.end()) info.IP = it->second;
|
||
|
||
it = kv.find("Location");
|
||
if (it != kv.end()) info.Location = it->second;
|
||
|
||
it = kv.find("LastActiveTime");
|
||
if (it != kv.end()) info.LastActiveTime = it->second;
|
||
|
||
it = kv.find("Status");
|
||
if (it != kv.end()) info.Status = it->second;
|
||
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
|
||
|
||
it = kv.find("PendingExpireDate");
|
||
if (it != kv.end()) info.PendingExpireDate = it->second;
|
||
|
||
it = kv.find("PendingHostNum");
|
||
if (it != kv.end()) info.PendingHostNum = atoi(it->second.c_str());
|
||
|
||
it = kv.find("PendingQuota");
|
||
if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str());
|
||
|
||
licenses.push_back(info);
|
||
}
|
||
|
||
return licenses;
|
||
}
|
||
|
||
void CLicenseDlg::InitListColumns()
|
||
{
|
||
m_ListLicense.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_LABELTIP | LVS_EX_INFOTIP);
|
||
|
||
m_ListLicense.InsertColumnL(0, _T("ID"), LVCFMT_LEFT, 35);
|
||
m_ListLicense.InsertColumnL(1, _T("序列号"), LVCFMT_LEFT, 125);
|
||
m_ListLicense.InsertColumnL(2, _T("状态"), LVCFMT_LEFT, 60);
|
||
m_ListLicense.InsertColumnL(3, _T("到期时间"), LVCFMT_LEFT, 80);
|
||
m_ListLicense.InsertColumnL(4, _T("备注"), LVCFMT_LEFT, 120);
|
||
m_ListLicense.InsertColumnL(5, _T("口令"), LVCFMT_LEFT, 260);
|
||
m_ListLicense.InsertColumnL(6, _T("版本"), LVCFMT_LEFT, 50);
|
||
m_ListLicense.InsertColumnL(7, _T("IP"), LVCFMT_LEFT, 190);
|
||
m_ListLicense.InsertColumnL(8, _T("位置"), LVCFMT_LEFT, 160);
|
||
m_ListLicense.InsertColumnL(9, _T("最后活跃"), LVCFMT_LEFT, 130);
|
||
m_ListLicense.InsertColumnL(10, _T("创建时间"), LVCFMT_LEFT, 130);
|
||
}
|
||
|
||
void CLicenseDlg::LoadLicenses()
|
||
{
|
||
m_Licenses = GetAllLicenses();
|
||
}
|
||
|
||
// 比较两个授权信息
|
||
static int CompareLicense(const LicenseInfo& a, const LicenseInfo& b, int nColumn)
|
||
{
|
||
switch (nColumn) {
|
||
case LIC_COL_SERIAL:
|
||
return a.SerialNumber.compare(b.SerialNumber);
|
||
case LIC_COL_STATUS:
|
||
return a.Status.compare(b.Status);
|
||
case LIC_COL_EXPIRE:
|
||
return a.PendingExpireDate.compare(b.PendingExpireDate);
|
||
case LIC_COL_REMARK:
|
||
return a.Remark.compare(b.Remark);
|
||
case LIC_COL_PASSCODE:
|
||
return a.Passcode.compare(b.Passcode);
|
||
case LIC_COL_HMAC:
|
||
return a.HMAC.compare(b.HMAC);
|
||
case LIC_COL_IP:
|
||
return a.IP.compare(b.IP);
|
||
case LIC_COL_LOCATION:
|
||
return a.Location.compare(b.Location);
|
||
case LIC_COL_LASTACTIVE:
|
||
return a.LastActiveTime.compare(b.LastActiveTime);
|
||
case LIC_COL_CREATETIME:
|
||
return a.CreateTime.compare(b.CreateTime);
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
void CLicenseDlg::RefreshList()
|
||
{
|
||
m_ListLicense.DeleteAllItems();
|
||
|
||
// 创建索引列表用于排序
|
||
std::vector<size_t> indices(m_Licenses.size());
|
||
for (size_t i = 0; i < indices.size(); ++i) {
|
||
indices[i] = i;
|
||
}
|
||
|
||
// 如果有排序列,进行排序
|
||
if (m_nSortColumn >= 0) {
|
||
int sortCol = m_nSortColumn;
|
||
BOOL ascending = m_bSortAscending;
|
||
std::sort(indices.begin(), indices.end(),
|
||
[this, sortCol, ascending](size_t a, size_t b) {
|
||
int result = CompareLicense(m_Licenses[a], m_Licenses[b], sortCol);
|
||
return ascending ? (result < 0) : (result > 0);
|
||
});
|
||
}
|
||
|
||
for (size_t i = 0; i < indices.size(); ++i) {
|
||
const auto& lic = m_Licenses[indices[i]];
|
||
CString strID;
|
||
strID.Format(_T("%d"), (int)(i + 1));
|
||
int idx = m_ListLicense.InsertItem((int)i, strID);
|
||
m_ListLicense.SetItemData(idx, indices[i]); // 保存原始索引
|
||
|
||
m_ListLicense.SetItemText(idx, 1, lic.SerialNumber.c_str());
|
||
|
||
// 显示到期时间
|
||
CString strPending;
|
||
std::string d = lic.PendingExpireDate;
|
||
if (d.empty()) {
|
||
// PendingExpireDate 为空,从 Passcode 中解析到期时间
|
||
d = ParseExpireDateFromPasscode(lic.Passcode);
|
||
}
|
||
if (d.length() == 8) {
|
||
// 格式化显示:20270221 -> 2027-02-21
|
||
strPending.Format(_T("%s-%s-%s"), d.substr(0, 4).c_str(),
|
||
d.substr(4, 2).c_str(), d.substr(6, 2).c_str());
|
||
} else if (!d.empty()) {
|
||
strPending = d.c_str();
|
||
}
|
||
m_ListLicense.SetItemText(idx, 3, strPending);
|
||
|
||
// 动态计算状态:根据到期时间自动调整 Active/Expired 状态
|
||
std::string displayStatus = lic.Status;
|
||
if (d.length() == 8 && lic.Status != LICENSE_STATUS_REVOKED) {
|
||
CTime now = CTime::GetCurrentTime();
|
||
CString strToday;
|
||
strToday.Format(_T("%04d%02d%02d"), now.GetYear(), now.GetMonth(), now.GetDay());
|
||
std::string today = CT2A(strToday);
|
||
|
||
if (d < today && lic.Status == LICENSE_STATUS_ACTIVE) {
|
||
// 已过期,标记为 Expired
|
||
displayStatus = LICENSE_STATUS_EXPIRED;
|
||
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_EXPIRED);
|
||
m_Licenses[indices[i]].Status = LICENSE_STATUS_EXPIRED;
|
||
} else if (d >= today && lic.Status == LICENSE_STATUS_EXPIRED) {
|
||
// 未过期(可能设置了预设续期),恢复为 Active
|
||
displayStatus = LICENSE_STATUS_ACTIVE;
|
||
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE);
|
||
m_Licenses[indices[i]].Status = LICENSE_STATUS_ACTIVE;
|
||
}
|
||
}
|
||
m_ListLicense.SetItemText(idx, 2, displayStatus.c_str());
|
||
|
||
m_ListLicense.SetItemText(idx, 4, lic.Remark.c_str());
|
||
m_ListLicense.SetItemText(idx, 5, lic.Passcode.c_str());
|
||
// 版本列:V1/V2
|
||
std::string versionDisplay = "V1";
|
||
if (lic.HMAC.length() >= 3 && lic.HMAC.substr(0, 3) == "v2:") {
|
||
versionDisplay = "V2";
|
||
}
|
||
m_ListLicense.SetItemText(idx, 6, versionDisplay.c_str());
|
||
// IP 列:多 IP 时显示 "[数量] 首个IP, ..."
|
||
m_ListLicense.SetItemText(idx, 7, FormatIPDisplay(lic.IP).c_str());
|
||
m_ListLicense.SetItemText(idx, 8, lic.Location.c_str());
|
||
m_ListLicense.SetItemText(idx, 9, lic.LastActiveTime.c_str());
|
||
m_ListLicense.SetItemText(idx, 10, lic.CreateTime.c_str());
|
||
}
|
||
}
|
||
|
||
void CLicenseDlg::OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||
{
|
||
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
|
||
int nColumn = pNMLV->iSubItem;
|
||
|
||
// ID列不排序
|
||
if (nColumn == LIC_COL_ID) {
|
||
*pResult = 0;
|
||
return;
|
||
}
|
||
|
||
// 点击同一列切换排序方向
|
||
if (nColumn == m_nSortColumn) {
|
||
m_bSortAscending = !m_bSortAscending;
|
||
} else {
|
||
m_nSortColumn = nColumn;
|
||
m_bSortAscending = TRUE;
|
||
}
|
||
|
||
SortByColumn(nColumn, m_bSortAscending);
|
||
*pResult = 0;
|
||
}
|
||
|
||
void CLicenseDlg::SortByColumn(int /*nColumn*/, BOOL /*bAscending*/)
|
||
{
|
||
m_ListLicense.SetRedraw(FALSE);
|
||
RefreshList();
|
||
m_ListLicense.SetRedraw(TRUE);
|
||
m_ListLicense.Invalidate();
|
||
}
|
||
|
||
BOOL CLicenseDlg::OnInitDialog()
|
||
{
|
||
__super::OnInitDialog();
|
||
|
||
// 设置对话框标题(解决英语系统乱码问题)
|
||
SetWindowText(_TR("授权管理"));
|
||
|
||
m_hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_PASSWORD));
|
||
SetIcon(m_hIcon, FALSE);
|
||
SetIcon(m_hIcon, TRUE);
|
||
|
||
InitListColumns();
|
||
LoadLicenses();
|
||
RefreshList();
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
void CLicenseDlg::OnSize(UINT nType, int cx, int cy)
|
||
{
|
||
__super::OnSize(nType, cx, cy);
|
||
|
||
if (m_ListLicense.GetSafeHwnd()) {
|
||
m_ListLicense.MoveWindow(7, 7, cx - 14, cy - 14);
|
||
}
|
||
}
|
||
|
||
// 更新授权状态
|
||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
// 检查授权是否存在
|
||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||
if (existingPasscode.empty()) {
|
||
return false; // 授权不存在
|
||
}
|
||
|
||
cfg.SetStr(deviceID, "Status", status);
|
||
return true;
|
||
}
|
||
|
||
void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
|
||
{
|
||
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
|
||
*pResult = 0;
|
||
|
||
int nItem = pNMItemActivate->iItem;
|
||
if (nItem < 0)
|
||
return;
|
||
|
||
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
|
||
if (nIndex >= m_Licenses.size())
|
||
return;
|
||
|
||
// 创建弹出菜单
|
||
CMenu menu;
|
||
menu.CreatePopupMenu();
|
||
|
||
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);
|
||
if (ipCount > 0) {
|
||
CString strViewIPs;
|
||
strViewIPs.Format(_TR("查看 IP 历史 (%d)(&I)..."), ipCount);
|
||
menu.AppendMenuL(MF_STRING, ID_LICENSE_VIEW_IPS, strViewIPs);
|
||
}
|
||
|
||
menu.AppendMenuL(MF_SEPARATOR, 0, _T(""));
|
||
if (lic.Status == LICENSE_STATUS_ACTIVE) {
|
||
menu.AppendMenuL(MF_STRING, ID_LICENSE_REVOKE, _T("撤销授权(&R)"));
|
||
} else {
|
||
menu.AppendMenuL(MF_STRING, ID_LICENSE_ACTIVATE, _T("激活授权(&A)"));
|
||
}
|
||
menu.AppendMenuL(MF_SEPARATOR, 0, _T(""));
|
||
menu.AppendMenuL(MF_STRING, ID_LICENSE_DELETE, _T("删除授权(&D)"));
|
||
|
||
// 显示菜单
|
||
CPoint point;
|
||
GetCursorPos(&point);
|
||
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseRevoke()
|
||
{
|
||
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];
|
||
if (SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_REVOKED)) {
|
||
m_Licenses[nIndex].Status = LICENSE_STATUS_REVOKED;
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_REVOKED);
|
||
}
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseActivate()
|
||
{
|
||
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];
|
||
if (SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE)) {
|
||
m_Licenses[nIndex].Status = LICENSE_STATUS_ACTIVE;
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_ACTIVE);
|
||
}
|
||
}
|
||
|
||
void CLicenseDlg::OnCancel()
|
||
{
|
||
// 隐藏窗口而不是关闭
|
||
ShowWindow(SW_HIDE);
|
||
}
|
||
|
||
void CLicenseDlg::OnOK()
|
||
{
|
||
// 不做任何操作,防止回车关闭对话框
|
||
}
|
||
|
||
void CLicenseDlg::ShowAndRefresh()
|
||
{
|
||
// 重新加载数据并显示
|
||
LoadLicenses();
|
||
RefreshList();
|
||
ShowWindow(SW_SHOW);
|
||
SetForegroundWindow();
|
||
}
|
||
|
||
// 从 passcode 解析过期日期(第2段,如 20260221)
|
||
std::string ParseExpireDateFromPasscode(const std::string& passcode)
|
||
{
|
||
size_t pos1 = passcode.find('-');
|
||
if (pos1 == std::string::npos) return "";
|
||
|
||
size_t pos2 = passcode.find('-', pos1 + 1);
|
||
if (pos2 == std::string::npos) return "";
|
||
|
||
return passcode.substr(pos1 + 1, pos2 - pos1 - 1);
|
||
}
|
||
|
||
// 从 passcode 解析并发连接数(第3段)
|
||
int ParseHostNumFromPasscode(const std::string& passcode)
|
||
{
|
||
size_t pos1 = passcode.find('-');
|
||
if (pos1 == std::string::npos) return 0;
|
||
|
||
size_t pos2 = passcode.find('-', pos1 + 1);
|
||
if (pos2 == std::string::npos) return 0;
|
||
|
||
size_t pos3 = passcode.find('-', pos2 + 1);
|
||
if (pos3 == std::string::npos) return 0;
|
||
|
||
std::string numStr = passcode.substr(pos2 + 1, pos3 - pos2 - 1);
|
||
return atoi(numStr.c_str());
|
||
}
|
||
|
||
// 设置待续期信息
|
||
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
// 检查授权是否存在
|
||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||
if (existingPasscode.empty()) {
|
||
return false;
|
||
}
|
||
|
||
cfg.SetStr(deviceID, "PendingExpireDate", expireDate);
|
||
cfg.SetInt(deviceID, "PendingHostNum", hostNum);
|
||
cfg.SetInt(deviceID, "PendingQuota", quota > 0 ? quota : 1);
|
||
return true;
|
||
}
|
||
|
||
// 获取待续期信息
|
||
RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
RenewalInfo info;
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
info.ExpireDate = cfg.GetStr(deviceID, "PendingExpireDate", "");
|
||
info.HostNum = cfg.GetInt(deviceID, "PendingHostNum", 0);
|
||
info.Quota = cfg.GetInt(deviceID, "PendingQuota", 1); // 默认配额为1
|
||
return info;
|
||
}
|
||
|
||
// 清除待续期信息
|
||
bool ClearPendingRenewal(const std::string& deviceID)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
cfg.SetStr(deviceID, "PendingExpireDate", "");
|
||
cfg.SetInt(deviceID, "PendingHostNum", 0);
|
||
cfg.SetInt(deviceID, "PendingQuota", 0);
|
||
return true;
|
||
}
|
||
|
||
// 配额递减,返回是否还有剩余配额
|
||
// 关键:read-modify-write 的 PendingQuota 必须在锁内完成,否则与 SetPendingRenewal
|
||
// 并发会丢失用户刚设置的预设续期(旧 bug:用户报告"预设续期消失"的根因)。
|
||
bool DecrementPendingQuota(const std::string& deviceID)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
int quota = cfg.GetInt(deviceID, "PendingQuota", 1);
|
||
if (quota <= 0) {
|
||
return false; // 无配额
|
||
}
|
||
|
||
quota--;
|
||
cfg.SetInt(deviceID, "PendingQuota", quota);
|
||
|
||
if (quota <= 0) {
|
||
// 配额用完,清除待续期信息(嵌套加锁,recursive_mutex 安全)
|
||
ClearPendingRenewal(deviceID);
|
||
return false;
|
||
}
|
||
return true; // 还有剩余配额
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseRenewal()
|
||
{
|
||
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];
|
||
|
||
// 从 passcode 解析当前过期日期和并发连接数
|
||
std::string currentExpireDate = ParseExpireDateFromPasscode(lic.Passcode);
|
||
int defaultHostNum = lic.PendingHostNum > 0 ? lic.PendingHostNum : ParseHostNumFromPasscode(lic.Passcode);
|
||
if (defaultHostNum <= 0) defaultHostNum = 100;
|
||
int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1;
|
||
|
||
// 格式化当前过期日期显示
|
||
CString strCurrentExpire;
|
||
if (currentExpireDate.length() == 8) {
|
||
strCurrentExpire.FormatL("当前到期: %s-%s-%s",
|
||
currentExpireDate.substr(0, 4).c_str(),
|
||
currentExpireDate.substr(4, 2).c_str(),
|
||
currentExpireDate.substr(6, 2).c_str());
|
||
} else {
|
||
strCurrentExpire = _TR("当前到期: 未知");
|
||
}
|
||
|
||
// 使用输入对话框获取续期信息(三个输入框)
|
||
CInputDialog dlg(this);
|
||
CString strTitle;
|
||
strTitle.FormatL("预设续期 (%s)", strCurrentExpire);
|
||
dlg.Init(strTitle, _L("续期天数:"));
|
||
dlg.Init2(_L("并发连接数:"), std::to_string(defaultHostNum).c_str());
|
||
dlg.Init3(_L("配额(机器数):"), std::to_string(defaultQuota).c_str());
|
||
|
||
if (dlg.DoModal() != IDOK)
|
||
return;
|
||
|
||
int days = atoi(dlg.m_str);
|
||
int hostNum = atoi(dlg.m_sSecondInput);
|
||
int quota = atoi(dlg.m_sThirdInput);
|
||
if (quota <= 0) quota = 1;
|
||
|
||
if (days <= 0 || hostNum <= 0) {
|
||
MessageBoxL("请输入有效的天数和并发连接数!", "错误", MB_ICONWARNING);
|
||
return;
|
||
}
|
||
|
||
// 计算新的过期日期 (从原到期日期开始加天数)
|
||
CTime baseTime;
|
||
if (currentExpireDate.length() == 8) {
|
||
int year = atoi(currentExpireDate.substr(0, 4).c_str());
|
||
int month = atoi(currentExpireDate.substr(4, 2).c_str());
|
||
int day = atoi(currentExpireDate.substr(6, 2).c_str());
|
||
baseTime = CTime(year, month, day, 0, 0, 0);
|
||
} else {
|
||
baseTime = CTime::GetCurrentTime(); // 无法解析时用当前日期
|
||
}
|
||
CTimeSpan span(days, 0, 0, 0);
|
||
CTime newExpireTime = baseTime + span;
|
||
CString strNewExpireDate;
|
||
strNewExpireDate.Format(_T("%04d%02d%02d"),
|
||
newExpireTime.GetYear(), newExpireTime.GetMonth(), newExpireTime.GetDay());
|
||
std::string newExpireDate = CT2A(strNewExpireDate);
|
||
|
||
if (SetPendingRenewal(lic.SerialNumber, newExpireDate, hostNum, quota)) {
|
||
m_Licenses[nIndex].PendingExpireDate = newExpireDate;
|
||
m_Licenses[nIndex].PendingHostNum = hostNum;
|
||
m_Licenses[nIndex].PendingQuota = quota;
|
||
|
||
// 续期后自动激活(如果之前是 Expired)
|
||
if (lic.Status == LICENSE_STATUS_EXPIRED) {
|
||
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE);
|
||
m_Licenses[nIndex].Status = LICENSE_STATUS_ACTIVE;
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_ACTIVE);
|
||
}
|
||
|
||
// 显示格式化的新过期日期
|
||
CString strPending;
|
||
strPending.Format(_T("%s-%s-%s"),
|
||
newExpireDate.substr(0, 4).c_str(),
|
||
newExpireDate.substr(4, 2).c_str(),
|
||
newExpireDate.substr(6, 2).c_str());
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_EXPIRE, strPending);
|
||
|
||
CString msg;
|
||
msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权",
|
||
strPending, hostNum, quota, quota);
|
||
MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION);
|
||
}
|
||
}
|
||
|
||
// 设置授权备注
|
||
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
// 检查授权是否存在
|
||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||
if (existingPasscode.empty()) {
|
||
return false;
|
||
}
|
||
|
||
cfg.SetStr(deviceID, "Remark", remark);
|
||
return true;
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseEditRemark()
|
||
{
|
||
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];
|
||
|
||
// 使用输入对话框编辑备注
|
||
CInputDialog dlg(this);
|
||
dlg.Init(_L("编辑备注"), _L("备注:"));
|
||
dlg.m_str = lic.Remark.c_str();
|
||
|
||
if (dlg.DoModal() != IDOK)
|
||
return;
|
||
|
||
std::string newRemark = CT2A(dlg.m_str);
|
||
if (SetLicenseRemark(lic.SerialNumber, newRemark)) {
|
||
m_Licenses[nIndex].Remark = newRemark;
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_REMARK, newRemark.c_str());
|
||
}
|
||
}
|
||
|
||
// 删除授权
|
||
bool DeleteLicense(const std::string& deviceID)
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
|
||
// 检查授权是否存在
|
||
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
|
||
if (existingPasscode.empty()) {
|
||
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;
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseDelete()
|
||
{
|
||
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];
|
||
|
||
// 确认删除
|
||
CString msg;
|
||
msg.FormatL("确定要删除授权 \"%s\" 吗?\n\n此操作不可撤销!", lic.SerialNumber.c_str());
|
||
if (MessageBox(msg, _TR("确认删除"), MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||
return;
|
||
}
|
||
|
||
if (DeleteLicense(lic.SerialNumber)) {
|
||
// 从列表和内存中删除
|
||
m_ListLicense.DeleteItem(nItem);
|
||
m_Licenses.erase(m_Licenses.begin() + nIndex);
|
||
|
||
// 刷新列表以更新 ID 编号
|
||
RefreshList();
|
||
} else {
|
||
MessageBoxL("删除授权失败!", "错误", MB_ICONERROR);
|
||
}
|
||
}
|
||
|
||
// 计算时间戳距今天数
|
||
// 支持6位格式 yyMMdd 和旧4位格式 MMdd
|
||
static int GetDaysFromTimestamp(const std::string& timestamp)
|
||
{
|
||
if (timestamp.empty()) return -1; // 无时间戳,返回-1表示无法判断
|
||
|
||
SYSTEMTIME now;
|
||
GetLocalTime(&now);
|
||
|
||
int year, month, day;
|
||
if (timestamp.length() == 6) {
|
||
// 新格式: yyMMdd (如 "260303" 表示 2026-03-03)
|
||
year = 2000 + atoi(timestamp.substr(0, 2).c_str());
|
||
month = atoi(timestamp.substr(2, 2).c_str());
|
||
day = atoi(timestamp.substr(4, 2).c_str());
|
||
} else if (timestamp.length() == 4) {
|
||
// 旧格式: MMdd,需要推断年份
|
||
month = atoi(timestamp.substr(0, 2).c_str());
|
||
day = atoi(timestamp.substr(2, 2).c_str());
|
||
// 假设是今年或去年(如果日期比今天晚则是去年)
|
||
year = now.wYear;
|
||
if (month > now.wMonth || (month == now.wMonth && day > now.wDay)) {
|
||
year--; // 日期比今天晚,应该是去年
|
||
}
|
||
} else {
|
||
return -1; // 无法解析
|
||
}
|
||
|
||
// 计算天数差
|
||
SYSTEMTIME st = { 0 };
|
||
st.wYear = (WORD)year;
|
||
st.wMonth = (WORD)month;
|
||
st.wDay = (WORD)day;
|
||
|
||
FILETIME ft1, ft2;
|
||
SystemTimeToFileTime(&st, &ft1);
|
||
SystemTimeToFileTime(&now, &ft2);
|
||
|
||
ULARGE_INTEGER u1, u2;
|
||
u1.LowPart = ft1.dwLowDateTime;
|
||
u1.HighPart = ft1.dwHighDateTime;
|
||
u2.LowPart = ft2.dwLowDateTime;
|
||
u2.HighPart = ft2.dwHighDateTime;
|
||
|
||
// 转换为天数 (100纳秒单位)
|
||
__int64 diff = (__int64)(u2.QuadPart - u1.QuadPart);
|
||
return (int)(diff / (10000000LL * 60 * 60 * 24));
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseViewIPs()
|
||
{
|
||
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]; // 非 const,因为可能需要更新
|
||
if (lic.IP.empty()) {
|
||
MessageBoxL("该授权暂无 IP 记录", "IP 历史", MB_ICONINFORMATION);
|
||
return;
|
||
}
|
||
|
||
const int EXPIRY_DAYS = 180; // 过期天数
|
||
|
||
// 解析 IP 列表
|
||
// 格式: "1.2.3.4(PC01)|260303, 1.2.3.4(PC02)|260215"
|
||
std::vector<IPRecord> records;
|
||
std::string newIPList; // 用于保存过滤后的 IP 列表
|
||
int removedCount = 0;
|
||
|
||
size_t start = 0;
|
||
while (start < lic.IP.length()) {
|
||
size_t end = lic.IP.find(',', start);
|
||
if (end == std::string::npos) end = lic.IP.length();
|
||
|
||
std::string entry = lic.IP.substr(start, end - start);
|
||
|
||
// 去除前后空格
|
||
size_t first = entry.find_first_not_of(' ');
|
||
size_t last = entry.find_last_not_of(' ');
|
||
if (first != std::string::npos && last != std::string::npos) {
|
||
entry = entry.substr(first, last - first + 1);
|
||
}
|
||
|
||
// 解析 IP|时间戳
|
||
size_t pipePos = entry.find('|');
|
||
std::string ip, timestamp;
|
||
if (pipePos != std::string::npos) {
|
||
ip = entry.substr(0, pipePos);
|
||
timestamp = entry.substr(pipePos + 1);
|
||
} else {
|
||
ip = entry;
|
||
}
|
||
|
||
if (!ip.empty()) {
|
||
// 检查是否过期
|
||
int daysAgo = GetDaysFromTimestamp(timestamp);
|
||
if (daysAgo >= 0 && daysAgo > EXPIRY_DAYS) {
|
||
// 记录已过期,跳过
|
||
removedCount++;
|
||
start = end + 1;
|
||
continue;
|
||
}
|
||
|
||
// 保留有效记录
|
||
if (!newIPList.empty()) newIPList += ", ";
|
||
newIPList += entry;
|
||
|
||
// 解析 IP 和机器名: "1.2.3.4(PC01)" -> ip="1.2.3.4", machine="PC01"
|
||
IPRecord record;
|
||
record.ip = ip;
|
||
record.timestamp = timestamp;
|
||
record.daysAgo = daysAgo;
|
||
|
||
size_t parenPos = ip.find('(');
|
||
if (parenPos != std::string::npos) {
|
||
record.ip = ip.substr(0, parenPos);
|
||
size_t endParen = ip.find(')', parenPos);
|
||
if (endParen != std::string::npos) {
|
||
record.machineName = ip.substr(parenPos + 1, endParen - parenPos - 1);
|
||
}
|
||
}
|
||
|
||
// 格式化日期
|
||
if (!timestamp.empty() && (timestamp.length() == 6 || timestamp.length() == 4)) {
|
||
std::string monthStr, dayStr;
|
||
if (timestamp.length() == 6) {
|
||
monthStr = timestamp.substr(2, 2);
|
||
dayStr = timestamp.substr(4, 2);
|
||
} else {
|
||
monthStr = timestamp.substr(0, 2);
|
||
dayStr = timestamp.substr(2, 2);
|
||
}
|
||
record.formattedDate = monthStr + "-" + dayStr;
|
||
}
|
||
|
||
records.push_back(record);
|
||
}
|
||
|
||
start = end + 1;
|
||
}
|
||
|
||
// 如果有记录被删除,保存更新后的 IP 列表
|
||
if (removedCount > 0) {
|
||
// 锁内只做 I/O —— UI 控件更新(SetItemText)放锁外,避免锁内触发
|
||
// 任何可能的消息循环回调,保持锁占用时间最短
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
||
}
|
||
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
|
||
|
||
// 更新列表显示(UI 线程操作,必须在锁外)
|
||
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
|
||
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
|
||
}
|
||
|
||
// 如果没有有效记录
|
||
if (records.empty()) {
|
||
CString msg;
|
||
msg.Format(_TR("所有 IP 记录均已过期(超过 %d 天),已自动清理"), EXPIRY_DAYS);
|
||
MessageBoxA(msg, _TR("IP 历史"), MB_ICONINFORMATION);
|
||
return;
|
||
}
|
||
|
||
// 使用对话框显示 IP 历史
|
||
CString strTitle;
|
||
strTitle.Format(_TR("IP 历史 - %s"), lic.SerialNumber.c_str());
|
||
|
||
CIPHistoryDlg dlg(this);
|
||
dlg.SetTitle(strTitle);
|
||
dlg.SetRecords(records);
|
||
dlg.SetRemovedCount(removedCount);
|
||
dlg.DoModal();
|
||
}
|
||
|
||
// 从 IP 列表字符串获取 IP 数量
|
||
int GetIPCountFromList(const std::string& ipListStr)
|
||
{
|
||
if (ipListStr.empty()) return 0;
|
||
|
||
int count = 1;
|
||
for (char c : ipListStr) {
|
||
if (c == ',') count++;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
// 从 IP 列表字符串获取第一个 IP(不含时间戳)
|
||
std::string GetFirstIPFromList(const std::string& ipListStr)
|
||
{
|
||
if (ipListStr.empty()) return "";
|
||
|
||
// 找到第一个逗号或字符串末尾
|
||
size_t commaPos = ipListStr.find(',');
|
||
std::string firstEntry = (commaPos != std::string::npos)
|
||
? ipListStr.substr(0, commaPos)
|
||
: ipListStr;
|
||
|
||
// 去除时间戳部分
|
||
size_t pipePos = firstEntry.find('|');
|
||
if (pipePos != std::string::npos) {
|
||
firstEntry = firstEntry.substr(0, pipePos);
|
||
}
|
||
|
||
// 去除前后空格
|
||
size_t first = firstEntry.find_first_not_of(' ');
|
||
size_t last = firstEntry.find_last_not_of(' ');
|
||
if (first != std::string::npos && last != std::string::npos) {
|
||
return firstEntry.substr(first, last - first + 1);
|
||
}
|
||
return firstEntry;
|
||
}
|
||
|
||
// 格式化 IP 显示: 多 IP 时显示 "[数量] IP1, IP2, IP3, ..."
|
||
std::string FormatIPDisplay(const std::string& ipListStr)
|
||
{
|
||
if (ipListStr.empty()) return "";
|
||
|
||
int count = GetIPCountFromList(ipListStr);
|
||
if (count <= 1) {
|
||
return GetFirstIPFromList(ipListStr);
|
||
}
|
||
|
||
// 解析前 3 个 IP(不含时间戳)
|
||
std::string ips[3];
|
||
int extracted = 0;
|
||
size_t start = 0;
|
||
while (start < ipListStr.length() && extracted < 3) {
|
||
size_t end = ipListStr.find(',', start);
|
||
if (end == std::string::npos) end = ipListStr.length();
|
||
|
||
std::string entry = ipListStr.substr(start, end - start);
|
||
// 去除前后空格
|
||
size_t first = entry.find_first_not_of(' ');
|
||
size_t last = entry.find_last_not_of(' ');
|
||
if (first != std::string::npos && last != std::string::npos) {
|
||
entry = entry.substr(first, last - first + 1);
|
||
}
|
||
// 去除时间戳
|
||
size_t pipePos = entry.find('|');
|
||
if (pipePos != std::string::npos) {
|
||
entry = entry.substr(0, pipePos);
|
||
}
|
||
if (!entry.empty()) {
|
||
ips[extracted++] = entry;
|
||
}
|
||
start = end + 1;
|
||
}
|
||
|
||
// 格式化: "[数量] IP1, IP2, IP3, ..."
|
||
char buf[256];
|
||
if (count <= 3) {
|
||
// 3 个或以下,全部显示
|
||
if (extracted == 2) {
|
||
sprintf_s(buf, "[%d] %s, %s", count, ips[0].c_str(), ips[1].c_str());
|
||
} else {
|
||
sprintf_s(buf, "[%d] %s, %s, %s", count, ips[0].c_str(), ips[1].c_str(), ips[2].c_str());
|
||
}
|
||
} else {
|
||
// 超过 3 个,显示前 3 个加 ...
|
||
sprintf_s(buf, "[%d] %s, %s, %s, ...", count, ips[0].c_str(), ips[1].c_str(), ips[2].c_str());
|
||
}
|
||
return buf;
|
||
}
|
||
|
||
// 检查 IP+机器名 是否在授权数据库中存在
|
||
// IP 字段格式: "1.2.3.4(PC01)|260218, 1.2.3.5(PC02)|260215" (yyMMdd)
|
||
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN)
|
||
{
|
||
if (ip.empty()) return false;
|
||
|
||
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
|
||
// GetAllLicenses 内部也加锁,recursive_mutex 允许嵌套。
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
auto licenses = GetAllLicenses();
|
||
for (const auto& lic : licenses) {
|
||
if (lic.IP.empty()) continue;
|
||
|
||
// 解析 IP 列表中的每个条目
|
||
size_t start = 0;
|
||
while (start < lic.IP.length()) {
|
||
size_t end = lic.IP.find(',', start);
|
||
if (end == std::string::npos) end = lic.IP.length();
|
||
|
||
std::string entry = lic.IP.substr(start, end - start);
|
||
|
||
// 去除前后空格
|
||
size_t first = entry.find_first_not_of(' ');
|
||
size_t last = entry.find_last_not_of(' ');
|
||
if (first != std::string::npos && last != std::string::npos) {
|
||
entry = entry.substr(first, last - first + 1);
|
||
}
|
||
|
||
// 去除时间戳部分 "|yyMMdd"
|
||
size_t pipePos = entry.find('|');
|
||
if (pipePos != std::string::npos) {
|
||
entry = entry.substr(0, pipePos);
|
||
}
|
||
|
||
// 解析 "IP(机器名)" 格式
|
||
std::string entryIP = entry, entryMachine;
|
||
size_t parenPos = entry.find('(');
|
||
if (parenPos != std::string::npos) {
|
||
entryIP = entry.substr(0, parenPos);
|
||
size_t endParen = entry.find(')', parenPos);
|
||
if (endParen != std::string::npos) {
|
||
entryMachine = entry.substr(parenPos + 1, endParen - parenPos - 1);
|
||
}
|
||
}
|
||
|
||
// 匹配 IP 和机器名(机器名可选匹配)
|
||
if (entryIP == ip) {
|
||
// IP 匹配,检查机器名
|
||
if (machineName.empty() || entryMachine.empty() || entryMachine == machineName) {
|
||
if (outSN) *outSN = lic.SerialNumber;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
start = end + 1;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 从 FrpConfig 字符串解析 remotePort
|
||
// 格式: "serverAddr:serverPort-remotePort-expireDate-authValue"
|
||
// 注意:serverAddr 可能是含 '-' 的域名(如 my-server.com),必须从右往左定位
|
||
// 与 CMy2015RemoteDlg::ParseFrpAutoConfig 的拆分策略一致。
|
||
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig)
|
||
{
|
||
if (frpConfig.empty()) return 0;
|
||
// 倒数第 1 个 '-':分隔 expireDate 与 authValue
|
||
size_t dashAuth = frpConfig.rfind('-');
|
||
if (dashAuth == std::string::npos || dashAuth == 0) return 0;
|
||
std::string s2 = frpConfig.substr(0, dashAuth);
|
||
// 倒数第 2 个 '-':分隔 remotePort 与 expireDate
|
||
size_t dashExpire = s2.rfind('-');
|
||
if (dashExpire == std::string::npos || dashExpire == 0) return 0;
|
||
std::string s3 = s2.substr(0, dashExpire);
|
||
// 倒数第 3 个 '-':分隔 serverAddr:serverPort 与 remotePort
|
||
size_t dashPort = s3.rfind('-');
|
||
if (dashPort == std::string::npos || dashPort == 0) return 0;
|
||
return atoi(s3.substr(dashPort + 1).c_str());
|
||
}
|
||
|
||
// 释放 frp_ports.ini 中指定端口的占用记录。
|
||
// 必须传入 expectedOwner —— 仅当端口当前的归属确实是它时才清除,
|
||
// 避免在 race 或外部改动后误抹掉别的 SN 的占用记录。
|
||
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner)
|
||
{
|
||
if (port <= 0 || expectedOwner.empty()) return false;
|
||
config portsCfg(CFrpsForSubDlg::GetFrpPortsPath());
|
||
char portStr[16];
|
||
sprintf_s(portStr, "%d", port);
|
||
std::string currentOwner = portsCfg.GetStr("ports", portStr, "");
|
||
if (currentOwner != expectedOwner) {
|
||
// 已被改写或释放,不动它
|
||
return false;
|
||
}
|
||
portsCfg.SetStr("ports", portStr, ""); // 写入空 owner,FindNextAvailablePort 将其视为可用
|
||
return true;
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseAutoFrp()
|
||
{
|
||
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
|
||
if (nItem < 0)
|
||
return;
|
||
|
||
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
|
||
if (nIndex >= m_Licenses.size())
|
||
return;
|
||
|
||
const auto& lic = m_Licenses[nIndex];
|
||
|
||
// 1. 前提条件:FRPS 服务器必须已在「下级 FRP 代理设置」中配置并启用
|
||
if (!CFrpsForSubDlg::IsFrpsConfigured()) {
|
||
MessageBoxL("请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token,然后再使用此功能。",
|
||
"未配置 FRPS", MB_OK | MB_ICONINFORMATION);
|
||
return;
|
||
}
|
||
|
||
// 2. 读取该授权当前的 FRP 配置(若已分配端口)
|
||
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||
|
||
// 已分配端口时,先询问是否覆盖
|
||
if (existingPort > 0) {
|
||
CString msg;
|
||
msg.FormatL("该授权已分配 FRPC 远程端口 %d,是否覆盖并重新设置?",
|
||
existingPort);
|
||
if (MessageBox(msg, _TR("覆盖 FRPC 配置"),
|
||
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 3. 决定默认端口:已有端口则沿用,否则查找下一个可用端口
|
||
int defaultPort = existingPort;
|
||
if (defaultPort <= 0) {
|
||
defaultPort = CFrpsForSubDlg::FindNextAvailablePort();
|
||
if (defaultPort <= 0) {
|
||
MessageBoxL("FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。",
|
||
"端口已满", MB_OK | MB_ICONWARNING);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 4. 弹出输入对话框,允许用户确认或修改端口
|
||
CInputDialog dlg(this);
|
||
CString strTitle;
|
||
strTitle.FormatL("自动 FRP - %s", lic.SerialNumber.c_str());
|
||
dlg.Init(strTitle, _L("FRPC 远程端口 (1024-65535):"));
|
||
CString strDefault;
|
||
strDefault.Format(_T("%d"), defaultPort);
|
||
dlg.m_str = strDefault;
|
||
|
||
if (dlg.DoModal() != IDOK)
|
||
return;
|
||
|
||
int remotePort = _ttoi(dlg.m_str);
|
||
if (remotePort < 1024 || remotePort > 65535) {
|
||
MessageBoxL("FRP 远程端口无效(1024-65535)", "提示", MB_OK | MB_ICONWARNING);
|
||
return;
|
||
}
|
||
|
||
// 5. 端口冲突检测:若端口已被其它序列号占用则拒绝
|
||
std::string portOwner = CFrpsForSubDlg::GetPortOwner(remotePort);
|
||
if (!portOwner.empty() && portOwner != lic.SerialNumber) {
|
||
CString msg;
|
||
msg.FormatL("端口 %d 已被序列号 %s 占用,请选择其它端口。",
|
||
remotePort, portOwner.c_str());
|
||
MessageBox(msg, _TR("端口冲突"), MB_OK | MB_ICONWARNING);
|
||
return;
|
||
}
|
||
|
||
// 6. 先生成 FRP 配置串(失败则直接返回,不动 frp_ports.ini)
|
||
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
|
||
// expireDate 固定 20371231(由 License 自身的过期控制实际有效性)
|
||
std::string frpConfig = GenerateFrpConfig(
|
||
frpsConfig.server, frpsConfig.port, remotePort,
|
||
frpsConfig.token, "20371231", frpsConfig.authMode);
|
||
|
||
if (frpConfig.empty()) {
|
||
MessageBoxL("生成 FRP 配置失败", "错误", MB_OK | MB_ICONERROR);
|
||
return;
|
||
}
|
||
|
||
// 7. 提交端口变更(顺序:新端口 RecordPortAllocation → 释放旧端口 → 写 licenses.ini)。
|
||
// 顺序保证:任意一步失败都不会出现"旧端口已释放 + 新端口未占用"的真空态。
|
||
CFrpsForSubDlg::RecordPortAllocation(remotePort, lic.SerialNumber);
|
||
if (existingPort > 0 && existingPort != remotePort) {
|
||
FreeFrpPortAllocation(existingPort, lic.SerialNumber); // 仅当旧端口确实归属本 SN 时才释放
|
||
}
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
cfg.SetStr(lic.SerialNumber, "FrpConfig", frpConfig);
|
||
}
|
||
|
||
CString msg;
|
||
msg.FormatL("已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。",
|
||
lic.SerialNumber.c_str(), remotePort);
|
||
MessageBox(msg, _TR("配置成功"), MB_OK | MB_ICONINFORMATION);
|
||
}
|
||
|
||
void CLicenseDlg::OnLicenseRevokeFrp()
|
||
{
|
||
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
|
||
if (nItem < 0)
|
||
return;
|
||
|
||
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
|
||
if (nIndex >= m_Licenses.size())
|
||
return;
|
||
|
||
const auto& lic = m_Licenses[nIndex];
|
||
|
||
// 读取当前 FRP 配置;若本就没有,菜单理应不显示,这里再防御一次
|
||
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
|
||
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
|
||
if (existingFrpConfig.empty() && existingPort <= 0) {
|
||
return;
|
||
}
|
||
|
||
// 二次确认(撤销后下级下次上线即丢失反向代理通道)
|
||
CString confirm;
|
||
confirm.FormatL("确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。",
|
||
lic.SerialNumber.c_str(), existingPort);
|
||
if (MessageBox(confirm, _TR("撤销 FRP 配置"),
|
||
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
|
||
return;
|
||
}
|
||
|
||
// 先释放 frp_ports.ini 中的端口占用(仅当归属仍为本 SN 时才释放)。
|
||
// 顺序:先释放端口,再清 FrpConfig —— 即便后一步失败,端口也已可被复用,
|
||
// 不会留下"FrpConfig 还在但端口已挂账"的悬空状态。
|
||
if (existingPort > 0) {
|
||
FreeFrpPortAllocation(existingPort, lic.SerialNumber);
|
||
}
|
||
|
||
// 清除 licenses.ini 中该授权的 FrpConfig 字段
|
||
{
|
||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||
std::string iniPath = GetLicensesPath();
|
||
config cfg(iniPath);
|
||
cfg.SetStr(lic.SerialNumber, "FrpConfig", "");
|
||
}
|
||
|
||
CString msg;
|
||
msg.FormatL("已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。",
|
||
lic.SerialNumber.c_str(), existingPort);
|
||
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
|
||
}
|