// 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 // 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 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; 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 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(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 _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(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 _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 _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 _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 _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 _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 _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 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 _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 _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 _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); }