Files
SimpleRemoter/server/2015Remote/CPasswordDlg.cpp
yuanyuanxiang 740ec8baf3 Perf(license): mutex + write-suppression for licenses.ini hot path
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.
2026-05-22 00:31:54 +02:00

1327 lines
51 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CPasswordDlg.cpp: 实现文件
//
#include "stdafx.h"
#include "CPasswordDlg.h"
#include "CLicenseDlg.h"
#include "afxdialogex.h"
#include "pwd_gen.h"
#include "2015Remote.h"
#include "common/skCrypter.h"
#include "2015RemoteDlg.h"
#include "LicenseFile.h"
#include "generated_hash.h"
#include "InputDlg.h"
#include "FrpsForSubDlg.h"
#include "UIBranding.h"
#include <unordered_map>
#include <ctime>
// 外部函数声明
extern std::vector<std::string> splitString(const std::string& str, char delimiter);
extern std::string GetFirstMasterIP(const std::string& master);
// ---- licenses.ini 并发与写抑制基础设施 (P1) ----
// 见 CPasswordDlg.h 中 LicensesIniMutex() 注释。这里给出实例。
std::recursive_mutex& LicensesIniMutex()
{
static std::recursive_mutex m;
return m;
}
namespace {
// UpdateLicenseActivity 的写抑制缓存:仅当字段实际变化或节流过期时才落盘。
//
// ⚠️ Cache key 是 "SN|IP|machine" 三元组而非单 SN因为同一 SN 可能被多个客户端
// 共用(团购授权场景:上百台机器共用一个序列号)。若按 SN 索引,多客户端的
// (IP, machine) 会在 cache 里反复互相覆盖 → ipChanged 几乎每次都为 true →
// 写抑制完全失效(实测从 0.6 次/秒只降到 0.7 次/秒)。
//
// 5s 心跳 × 100 客户端,每客户端独立 30s 节流后 → 100/30 ≈ 3.3 次落盘/秒。
// Passcode/HMAC 是 per-SN 的,按本结构会在每个客户端的 entry 里冗余存一份,
// 续期换码时所有客户端会各自触发一次重写(写入同一新值),冗余但无害。
struct LicenseActivityCache {
time_t lastFlushTime = 0; // 上次实际落盘的 epoch 秒
std::string lastPasscode; // 上次写入 ini 的 Passcode
std::string lastHMAC; // 上次写入 ini 的 HMAC
std::string lastLocation; // 上次写入 ini 的 Location
std::string lastIPWriteDate; // 上次写 IP 列表时的日期 yyMMdd
};
// Key 格式:"SN|IP|machine"。IP/machine 可能为空ctx == null 路径),
// 此时 key 形如 "SN||" —— 该路径自成一类节流域,互不干扰。
std::unordered_map<std::string, LicenseActivityCache> g_activityCache;
// 30 秒节流窗口LastActiveTime 最多 30 秒落盘一次(即便其它字段未变)。
// UI 显示的"最后活跃"最多延迟 30 秒,业务可接受。
constexpr int LAST_ACTIVE_THROTTLE_SECONDS = 30;
}
// 由 DeleteLicense 等"在 cache 视野外修改了 disk"的路径调用,清掉某 SN 名下所有
// (IP, machine) entry强制下次 UpdateLicenseActivity 走 firstTime 路径重建 section。
// Cache key 形如 "SN|IP|machine",同一 SN 可能对应多个 entry多客户端共用授权
// 必须按前缀遍历清除。
void InvalidateLicenseActivityCache(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
const std::string prefix = deviceID + "|";
for (auto it = g_activityCache.begin(); it != g_activityCache.end(); ) {
if (it->first.compare(0, prefix.size(), prefix) == 0) {
it = g_activityCache.erase(it);
} else {
++it;
}
}
}
// CPasswordDlg 对话框
IMPLEMENT_DYNAMIC(CPasswordDlg, CDialogEx)
void WriteHash(const char* pwdHash, const char* upperHash) {
if (strlen(pwdHash) == 0 || strlen(upperHash) == 0) {
return;
}
memcpy(g_UpperHash + 100, upperHash, 64);
std::string id = genHMACbyHash(pwdHash, upperHash);
Validation verify(365, "", 0, id.c_str());
WritePwdHash(g_MasterID, pwdHash, verify);
}
std::string getUpperHash()
{
return std::string(g_UpperHash + 100, 64);
}
std::string GetUpperHash()
{
// Check if upper hash is set (first byte not null)
if (g_UpperHash[100] == '\0') {
return GetMasterHash();
}
return std::string(g_UpperHash + 100, 64);
}
std::string GetPwdHash()
{
return std::string(g_MasterID, 64);
}
const Validation * GetValidation(int offset)
{
return (Validation*)(g_MasterID + offset);
}
std::string GetMasterId()
{
auto id = std::string(g_MasterID).substr(0, 16);
return id;
}
std::string GetHMAC(int offset)
{
const Validation * v= (Validation*)(g_MasterID + offset);
std::string hmac(v->Checksum, 16);
if (hmac.c_str()[0] == 0)
hmac = THIS_CFG.GetStr("settings", "HMAC");
return hmac;
}
void SetHMAC(const std::string str, int offset)
{
Validation* v = (Validation*)(g_MasterID + offset);
std::string hmac(v->Checksum, 16);
if (hmac.c_str()[0] == 0) {
memcpy(v->Checksum, str.c_str(), min(16, str.length()));
THIS_CFG.SetStr("settings", "HMAC", str);
}
}
extern "C" void shrink64to32(const char* input64, char* output32); // output32 必须至少 33 字节
extern "C" void shrink32to4(const char* input32, char* output4); // output4 必须至少 5 字节
// 获取授权信息存储路径
std::string GetLicensesPath()
{
std::string dbPath = GetDbPath();
// GetDbPath() 返回完整文件路径如 "C:\...\YAMA\YAMA.db"
// 需要去掉末尾文件名,保留目录部分
size_t pos = dbPath.find_last_of("\\/");
if (pos != std::string::npos) {
return dbPath.substr(0, pos + 1) + "licenses.ini";
}
return "licenses.ini";
}
// 保存授权信息到 INI 文件
bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
const std::string& hmac, const std::string& remark,
const std::string& authorization,
const std::string& frpConfig)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 以 DeviceID 为 section 名
cfg.SetStr(deviceID, "SerialNumber", deviceID);
cfg.SetStr(deviceID, "Passcode", passcode);
cfg.SetStr(deviceID, "HMAC", hmac);
cfg.SetStr(deviceID, "Remark", remark);
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE); // 默认状态为有效
// 保存 Authorization多层授权
// 注意authorization 参数传入时已经是混淆后的格式,直接保存
if (!authorization.empty()) {
cfg.SetStr(deviceID, "Authorization", authorization);
}
// 保存 FRP 配置(用于为下级提供 FRP 代理)
if (!frpConfig.empty()) {
cfg.SetStr(deviceID, "FrpConfig", frpConfig);
}
// 保存创建时间
SYSTEMTIME st;
GetLocalTime(&st);
char timeStr[32];
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
cfg.SetStr(deviceID, "CreateTime", timeStr);
// 初始化扩展字段
cfg.SetStr(deviceID, "IP", "");
cfg.SetStr(deviceID, "Location", "");
cfg.SetStr(deviceID, "LastActiveTime", "");
return true;
}
// 加载授权信息
bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
std::string& hmac, std::string& remark)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
passcode = cfg.GetStr(deviceID, "Passcode", "");
if (passcode.empty())
return false;
hmac = cfg.GetStr(deviceID, "HMAC", "");
remark = cfg.GetStr(deviceID, "Remark", "");
return true;
}
// 加载授权的 FRP 配置
std::string LoadLicenseFrpConfig(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
return cfg.GetStr(deviceID, "FrpConfig", "");
}
// 加载授权的 Authorization用于 V2 授权返回给第一层)
std::string LoadLicenseAuthorization(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
return cfg.GetStr(deviceID, "Authorization", "");
}
// 更新授权的 AuthorizationV2 续期时更新)
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization)
{
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, "Authorization", authorization);
return true;
}
// IP 列表管理常量
#define MAX_IP_HISTORY 500 // 最多保留 几 个不同的 IP
// 解析 IP 列表字符串为 vector<pair<IP, 时间戳>>
// 格式: "192.168.1.1|260218, 10.0.0.1|260215" (yyMMdd)
static std::vector<std::pair<std::string, std::string>> ParseIPList(const std::string& ipListStr)
{
std::vector<std::pair<std::string, std::string>> result;
if (ipListStr.empty()) return result;
size_t start = 0;
while (start < ipListStr.length()) {
// 找到下一个逗号
size_t end = ipListStr.find(',', start);
if (end == std::string::npos) end = ipListStr.length();
// 提取单个 IP 条目
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);
}
// 解析 IP|时间戳
size_t pipePos = entry.find('|');
if (pipePos != std::string::npos) {
std::string ip = entry.substr(0, pipePos);
std::string ts = entry.substr(pipePos + 1);
result.push_back({ ip, ts });
} else if (!entry.empty()) {
// 兼容旧格式(无时间戳)
result.push_back({ entry, "" });
}
start = end + 1;
}
return result;
}
// 将 IP 列表序列化为字符串
static std::string SerializeIPList(const std::vector<std::pair<std::string, std::string>>& ipList)
{
std::string result;
for (size_t i = 0; i < ipList.size(); ++i) {
if (i > 0) result += ", ";
result += ipList[i].first;
if (!ipList[i].second.empty()) {
result += "|";
result += ipList[i].second;
}
}
return result;
}
// 更新 IP 列表:添加新 IP 或更新已有 IP 的时间戳
// 格式: IP(机器名)|时间戳,例如 "1.2.3.4(PC01)|260219" (yyMMdd 格式)
// 返回更新后的 IP 列表字符串
static std::string UpdateIPList(const std::string& existingIPList, const std::string& newIP, const std::string& machineName = "")
{
if (newIP.empty()) return existingIPList;
// 构造 IP 标识:如果有机器名则为 "IP(机器名)",否则为 "IP"
std::string ipKey = newIP;
if (!machineName.empty()) {
// 机器名可能包含 "/"(如 "PC01/GroupName"),只取第一部分
std::string shortName = machineName;
size_t slashPos = machineName.find('/');
if (slashPos != std::string::npos) {
shortName = machineName.substr(0, slashPos);
}
ipKey = newIP + "(" + shortName + ")";
}
// 获取当前时间戳 (yyMMdd 格式6位)
SYSTEMTIME st;
GetLocalTime(&st);
char timestamp[8];
sprintf_s(timestamp, "%02d%02d%02d", st.wYear % 100, st.wMonth, st.wDay);
// 解析现有 IP 列表
auto ipList = ParseIPList(existingIPList);
// 提取纯 IP去掉括号内的机器名
auto extractIP = [](const std::string& s) -> std::string {
size_t pos = s.find('(');
return pos != std::string::npos ? s.substr(0, pos) : s;
};
// 查找是否已存在该 IP只比较 IP 部分,兼容旧格式)
bool found = false;
for (auto& entry : ipList) {
if (extractIP(entry.first) == newIP) {
entry.first = ipKey; // 更新为新格式(带机器名)
entry.second = timestamp; // 更新时间戳
found = true;
break;
}
}
if (!found) {
// 新 IP添加到开头最近的在前
ipList.insert(ipList.begin(), { ipKey, timestamp });
// 限制数量
if (ipList.size() > MAX_IP_HISTORY) {
ipList.resize(MAX_IP_HISTORY);
}
}
return SerializeIPList(ipList);
}
// 获取 IP 列表中的 IP 数量
static int GetIPCount(const std::string& ipListStr)
{
if (ipListStr.empty()) return 0;
auto ipList = ParseIPList(ipListStr);
return (int)ipList.size();
}
// 更新授权活跃信息(带写抑制)
//
// 设计要点:心跳每 5 秒触发一次本函数;同样的 SN 在稳态下绝大多数字段不会变化。
// 朴素实现每次心跳都做 6-8 次 SetStr (整文件重写)5 秒就是一轮全文件 I/O 风暴,
// 100 在线时会饱和。本实现引入 in-memory 缓存 g_activityCache
// - 字段未变化 + LastActiveTime 节流窗口30 秒)内 → 直接 return零 I/O
// - 字段变化passcode/HMAC/IP/Location 任一)→ 仅写变化字段
// - 节流过期 → 只写 LastActiveTime轻量刷新
// - IP 列表中的时间戳是日级精度yyMMdd跨天必须重写一次以刷新日期
//
// 注意:仅在落盘成功后才更新 cache保证 cache 永远反映"磁盘上当前值"。
bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
const std::string& hmac, const std::string& ip,
const std::string& location, const std::string& machineName)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
// Cache key 是 (SN, IP, machine) 三元组 —— 同 SN 多客户端共用授权时各自独立节流。
// 若同 SN 不同 (ip, machine) 共用一个 cache entryIP 字段会在不同客户端间反复
// 翻转,每次心跳都判定为 ipChanged → 写抑制完全失效。
const std::string cacheKey = deviceID + "|" + ip + "|" + machineName;
auto& cache = g_activityCache[cacheKey];
time_t now = time(nullptr);
// 计算今日日期串yyMMdd用于和 IP 列表时间戳比对
SYSTEMTIME st;
GetLocalTime(&st);
char today[8];
sprintf_s(today, "%02d%02d%02d", st.wYear % 100, st.wMonth, st.wDay);
// —— 决策阶段:判断本次心跳是否真的需要落盘 ——
// 注意:因 cacheKey 已经包含 (IP, machine),不同的客户端会落到不同 entry
// 所以不再需要在字段比对中处理 IP/machine 变化 —— 那种"变化"其实是 cache miss。
const bool firstTime = (cache.lastFlushTime == 0);
const bool passcodeChanged = (passcode != cache.lastPasscode);
const bool hmacChanged = (hmac != cache.lastHMAC);
const bool ipDayChanged = !ip.empty() && !cache.lastIPWriteDate.empty() &&
std::string(today) != cache.lastIPWriteDate;
const bool locationChanged = !location.empty() && (location != cache.lastLocation);
const bool throttleExpired = (now - cache.lastFlushTime >= LAST_ACTIVE_THROTTLE_SECONDS);
if (!firstTime && !passcodeChanged && !hmacChanged
&& !ipDayChanged && !locationChanged && !throttleExpired) {
// 100% cache 命中:本客户端的所有字段都与上次落盘一致且节流未过期
return true;
}
// —— 落盘阶段:仅写真正需要写的字段 ——
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 检查该授权是否存在(注意:此处仍需读磁盘,因为我们不缓存"是否存在"的事实)
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
const bool isNewRecord = existingPasscode.empty();
if (isNewRecord) {
// 授权不存在但验证成功 —— 既往授权自动加入
cfg.SetStr(deviceID, "SerialNumber", deviceID);
cfg.SetStr(deviceID, "Passcode", passcode);
cfg.SetStr(deviceID, "HMAC", hmac);
cfg.SetStr(deviceID, "Remark", "既往授权自动加入");
cfg.SetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
} else {
// 已存在:只在 passcode/hmac 实际变化时才写(续期场景才会变)
if (firstTime || passcodeChanged) {
cfg.SetStr(deviceID, "Passcode", passcode);
}
if (firstTime || hmacChanged) {
cfg.SetStr(deviceID, "HMAC", hmac);
}
}
// LastActiveTime走到这里就更新节流过期或字段变化都需要刷新
char timeStr[32];
sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
cfg.SetStr(deviceID, "LastActiveTime", timeStr);
if (isNewRecord) {
cfg.SetStr(deviceID, "CreateTime", timeStr);
}
// IP 列表:本客户端首次 或 同客户端跨天 才重写UpdateIPList 会在 disk 上合并)
if (!ip.empty() && (firstTime || ipDayChanged)) {
std::string existingIPList = cfg.GetStr(deviceID, "IP", "");
std::string newIPList = UpdateIPList(existingIPList, ip, machineName);
cfg.SetStr(deviceID, "IP", newIPList);
cache.lastIPWriteDate = today;
}
if (!location.empty() && (firstTime || locationChanged)) {
cfg.SetStr(deviceID, "Location", location);
}
// —— 同步缓存(必须在落盘成功后)——
cache.lastFlushTime = now;
cache.lastPasscode = passcode;
cache.lastHMAC = hmac;
if (!location.empty()) {
cache.lastLocation = location;
}
return true;
}
// 检查授权是否已被撤销
bool IsLicenseRevoked(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
std::string status = cfg.GetStr(deviceID, "Status", LICENSE_STATUS_ACTIVE);
return (status == LICENSE_STATUS_REVOKED);
}
#ifdef _WIN64
#pragma comment(lib, "lib/shrink_x64.lib")
#else
#pragma comment(lib, "lib/shrink.lib")
#endif
std::string GetFinderString(const char* buf)
{
char output32[100] = {};
memcpy(output32, buf, 64);
if (GetPwdHash() == GetMasterHash()) {
return std::string(output32, 100);
}
shrink64to32(buf, output32+64);
return std::string(output32, 96);
}
bool WritePwdHash(char* target, const std::string & pwdHash, const Validation& verify)
{
char output32[33], output4[5];
shrink64to32(pwdHash.c_str(), output32);
shrink32to4(output32, output4);
if (output32[0] == 0 || output4[0] == 0)
return false;
memcpy(target, pwdHash.c_str(), pwdHash.length());
memcpy(target + 64, output32, 32);
memcpy(target + 96, output4, 4);
#ifdef _DEBUG
ASSERT(IsPwdHashValid(target));
#endif
memcpy(target+100, &verify, sizeof(verify));
return true;
}
bool IsPwdHashValid(const char* hash)
{
const char* ptr = hash ? hash : g_MasterID;
if (ptr == GetMasterHash())
return true;
std::string pwdHash(ptr, 64), s1(ptr +64, 32), s2(ptr +96, 4);
char output32[33], output4[5];
shrink64to32(pwdHash.c_str(), output32);
shrink32to4(output32, output4);
if (memcmp(output32, s1.c_str(), 32) || memcmp(output4, s2.c_str(), 4)) {
// 哈希校验失败,尝试离线校验 Authorization
std::string authObf = THIS_CFG.GetStr("settings", "Authorization", "");
if (!authObf.empty() && TcpClient::IsAuthorizationValid(authObf)) {
return true; // Authorization 签名有效
}
return false;
}
return true;
}
CPasswordDlg::CPasswordDlg(CWnd* pParent /*=nullptr*/)
: CDialogLangEx(IDD_DIALOG_PASSWORD, pParent)
, m_sDeviceID(_T(""))
, m_sPassword(_T(""))
, m_sPasscodeHmac(THIS_CFG.GetStr("settings", "PwdHmac", "").c_str())
, m_nBindType(THIS_CFG.GetInt("settings", "BindType", 0))
{
m_hIcon = nullptr;
}
CPasswordDlg::~CPasswordDlg()
{
}
void CPasswordDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_DEVICEID, m_EditDeviceID);
DDX_Control(pDX, IDC_EDIT_DEVICEPWD, m_EditPassword);
DDX_Text(pDX, IDC_EDIT_DEVICEID, m_sDeviceID);
DDV_MaxChars(pDX, m_sDeviceID, 19);
DDX_Text(pDX, IDC_EDIT_DEVICEPWD, m_sPassword);
DDV_MaxChars(pDX, m_sPassword, 42);
DDX_Control(pDX, IDC_COMBO_BIND, m_ComboBinding);
DDX_Control(pDX, IDC_EDIT_PASSCODE_HMAC, m_EditPasscodeHmac);
DDX_Text(pDX, IDC_EDIT_PASSCODE_HMAC, m_sPasscodeHmac);
DDX_Control(pDX, IDC_EDIT_ROOT_CERT, m_EditRootCert);
DDX_Text(pDX, IDC_EDIT_ROOT_CERT, m_sRootCert);
DDX_CBIndex(pDX, IDC_COMBO_BIND, m_nBindType);
}
BEGIN_MESSAGE_MAP(CPasswordDlg, CDialogEx)
ON_CBN_SELCHANGE(IDC_COMBO_BIND, &CPasswordDlg::OnCbnSelchangeComboBind)
END_MESSAGE_MAP()
BOOL CPasswordDlg::OnInitDialog()
{
__super::OnInitDialog();
// 多语言翻译 - Static控件
SetDlgItemText(IDC_STATIC_PASSWORD_SERIAL, _TR("序 列 号:"));
SetDlgItemText(IDC_STATIC_PASSWORD_TOKEN, _TR("授权口令:"));
SetDlgItemText(IDC_STATIC_PASSWORD_METHOD, _TR("授权方式:"));
SetDlgItemText(IDC_STATIC_PASSWORD_VERIFY, _TR("验 证 码:"));
SetDlgItemText(IDC_STATIC_ROOT_CERT, _TR("根 凭 证:"));
// 设置对话框标题和控件文本(解决英语系统乱码问题)
SetWindowText(_TR("口令"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
// TODO: 在此添加额外的初始化
m_hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_PASSWORD));
SetIcon(m_hIcon, FALSE);
m_ComboBinding.InsertStringL(0, "计算机硬件信息");
m_ComboBinding.InsertStringL(1, "主控IP或域名信息");
m_ComboBinding.SetCurSel(m_nBindType);
// 加载已保存的根凭证
std::string savedAuth = THIS_CFG.GetStr("settings", "Authorization", "");
if (!savedAuth.empty()) {
m_sRootCert = savedAuth.c_str();
m_EditRootCert.SetWindowText(m_sRootCert);
}
return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}
void CPasswordDlg::OnCbnSelchangeComboBind()
{
m_nBindType = m_ComboBinding.GetCurSel();
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID(m_nBindType);
m_sDeviceID = getFixedLengthID(hashSHA256(hardwareID)).c_str();
m_EditDeviceID.SetWindowTextA(m_sDeviceID);
auto master = THIS_CFG.GetStr("settings", "master", "");
if (m_nBindType == 1) {
MessageBoxL(_L("请确认是否正确设置公网地址IP或域名\r\n"
"绑定IP后主控只能使用指定IP绑定域名后\r\n"
"主控只能使用指定域名。当前公网地址: \r\n")+
+ CString(master.empty() ? _L("未设置") : master.c_str()), "提示", MB_OK | MB_ICONWARNING);
}
}
void CPasswordDlg::OnOK()
{
UpdateData(TRUE);
if (!m_sDeviceID.IsEmpty()) {
THIS_CFG.SetInt("settings", "BindType", m_nBindType);
THIS_CFG.SetStr("settings", "SN", m_sDeviceID.GetString()); // 切换绑定方式时同步保存 SN
THIS_CFG.SetStr("settings", "PwdHmac", m_sPasscodeHmac.GetString());
// 保存根凭证(可选,用于多层授权)
if (!m_sRootCert.IsEmpty()) {
THIS_CFG.SetStr("settings", "Authorization", m_sRootCert.GetString());
}
}
__super::OnOK();
}
// CPasswordDlg 消息处理程序
IMPLEMENT_DYNAMIC(CPwdGenDlg, CDialogEx)
CPwdGenDlg::CPwdGenDlg(CWnd* pParent /*=nullptr*/)
: CDialogLangEx(IDD_DIALOG_KEYGEN, pParent)
, m_sDeviceID(_T(""))
, m_sPassword(_T(""))
, m_sUserPwd(_T(""))
, m_sHMAC(_T(""))
, m_ExpireTm(COleDateTime::GetCurrentTime())
, m_StartTm(COleDateTime::GetCurrentTime())
, m_nHostNum(2)
, m_nAuthHostNum(2) // 默认与连接数相同
, m_bAuthHostNumManual(FALSE) // 未手动修改
, m_bIsLocalDevice(FALSE)
, m_nVersion(0)
, m_sPrivateKeyPath(_T(""))
, m_nFrpRemotePort(0)
{
}
CPwdGenDlg::~CPwdGenDlg()
{
}
void CPwdGenDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_DEVICEID, m_EditDeviceID);
DDX_Control(pDX, IDC_EDIT_DEVICEPWD, m_EditPassword);
DDX_Control(pDX, IDC_EDIT_USERPWD, m_EditUserPwd);
DDX_Text(pDX, IDC_EDIT_DEVICEID, m_sDeviceID);
DDV_MaxChars(pDX, m_sDeviceID, 19);
DDX_Text(pDX, IDC_EDIT_DEVICEPWD, m_sPassword);
DDV_MaxChars(pDX, m_sPassword, 42);
DDX_Text(pDX, IDC_EDIT_USERPWD, m_sUserPwd);
DDV_MaxChars(pDX, m_sUserPwd, 24);
DDX_Control(pDX, IDC_EXPIRE_DATE, m_PwdExpireDate);
DDX_DateTimeCtrl(pDX, IDC_EXPIRE_DATE, m_ExpireTm);
DDX_Control(pDX, IDC_START_DATE, m_StartDate);
DDX_DateTimeCtrl(pDX, IDC_START_DATE, m_StartTm);
DDX_Control(pDX, IDC_EDIT_HOSTNUM, m_EditHostNum);
DDX_Text(pDX, IDC_EDIT_HOSTNUM, m_nHostNum);
DDV_MinMaxInt(pDX, m_nHostNum, 2, 10000);
DDX_Control(pDX, IDC_EDIT_HMAC, m_EditHMAC);
DDX_Control(pDX, IDC_EDIT_AUTHORIZATION, m_EditAuthorization);
DDX_Control(pDX, IDC_BUTTON_SAVE_LICENSE, m_BtnSaveLicense);
DDX_Text(pDX, IDC_EDIT_HMAC, m_sHMAC);
DDX_Text(pDX, IDC_EDIT_AUTHORIZATION, m_sAuthorization);
// V2 Authorization 下级并发数
DDX_Control(pDX, IDC_EDIT_AUTH_HOSTNUM, m_EditAuthHostNum);
DDX_Text(pDX, IDC_EDIT_AUTH_HOSTNUM, m_nAuthHostNum);
DDV_MinMaxInt(pDX, m_nAuthHostNum, 2, 10000);
DDX_Control(pDX, IDC_COMBO_VERSION, m_ComboVersion);
DDX_Control(pDX, IDC_EDIT_PRIVATEKEY, m_EditPrivateKey);
DDX_Control(pDX, IDC_BUTTON_BROWSE_KEY, m_BtnBrowseKey);
DDX_Control(pDX, IDC_BUTTON_GEN_KEYPAIR, m_BtnGenKeyPair);
DDX_CBIndex(pDX, IDC_COMBO_VERSION, m_nVersion);
DDX_Text(pDX, IDC_EDIT_PRIVATEKEY, m_sPrivateKeyPath);
// FRP 控件
DDX_Control(pDX, IDC_CHECK_FRP_PROXY, m_CheckFrpProxy);
DDX_Control(pDX, IDC_EDIT_FRP_REMOTE_PORT, m_EditFrpRemotePort);
DDX_Control(pDX, IDC_BTN_FRP_AUTO_PORT, m_BtnFrpAutoPort);
DDX_Control(pDX, IDC_STATIC_FRP_INFO, m_StaticFrpInfo);
DDX_Text(pDX, IDC_EDIT_FRP_REMOTE_PORT, m_nFrpRemotePort);
}
BEGIN_MESSAGE_MAP(CPwdGenDlg, CDialogEx)
ON_BN_CLICKED(IDC_BUTTON_GENKEY, &CPwdGenDlg::OnBnClickedButtonGenkey)
ON_BN_CLICKED(IDC_BUTTON_SAVE_LICENSE, &CPwdGenDlg::OnBnClickedButtonSaveLicense)
ON_CBN_SELCHANGE(IDC_COMBO_VERSION, &CPwdGenDlg::OnCbnSelchangeComboVersion)
ON_BN_CLICKED(IDC_BUTTON_BROWSE_KEY, &CPwdGenDlg::OnBnClickedButtonBrowseKey)
ON_BN_CLICKED(IDC_BUTTON_GEN_KEYPAIR, &CPwdGenDlg::OnBnClickedButtonGenKeypair)
ON_BN_CLICKED(IDC_CHECK_FRP_PROXY, &CPwdGenDlg::OnBnClickedCheckFrpProxy)
ON_BN_CLICKED(IDC_BTN_FRP_AUTO_PORT, &CPwdGenDlg::OnBnClickedBtnFrpAutoPort)
ON_EN_CHANGE(IDC_EDIT_HOSTNUM, &CPwdGenDlg::OnEnChangeEditHostNum)
ON_EN_CHANGE(IDC_EDIT_AUTH_HOSTNUM, &CPwdGenDlg::OnEnChangeEditAuthHostNum)
END_MESSAGE_MAP()
// 构建 V1 Authorization第一层/下级返回给下级)
std::string BuildV1Authorization(const std::string& sn, bool heartbeat) {
std::string storedAuthObf = THIS_CFG.GetStr("settings", "Authorization", "");
std::string storedAuth = TcpClient::DeobfuscateAuthorization(storedAuthObf);
std::string masterIP;
int masterPort;
CMy2015RemoteDlg::GetEffectiveMasterAddress(masterIP, masterPort, heartbeat);
std::string hmacServer = masterIP.empty() ? "" : masterIP + ":" + std::to_string(masterPort);
if (storedAuth.empty() || hmacServer.empty()) {
return "";
}
std::string authStr;
auto authParts = splitString(storedAuth, '|');
if (authParts.size() == 5) {
// V2 格式5段第一层给第二层
// 验证 snHashPrefix 匹配(防止使用其他第一层的 Authorization
std::string storedPrefix = authParts[3];
std::string mySN = THIS_CFG.GetStr("settings", "SN", "");
std::string expectedPrefix = computeSnHashPrefix(mySN);
if (storedPrefix != expectedPrefix) {
Mprintf("V1 Authorization snHashPrefix 不匹配: 期望 %s, 存储 %s\n",
expectedPrefix.c_str(), storedPrefix.c_str());
return "";
}
// 组装完整 AuthorizationV16段
authStr = authParts[0] + "|" + authParts[1] + "|" + authParts[2] + "|" + authParts[3] + "|" + hmacServer + "|" + authParts[4];
if (!heartbeat)
Mprintf("V1 返回 Authorization: %s. AuthServer: %s\n", sn.c_str(), hmacServer.c_str());
} else if (authParts.size() == 6) {
// V1 格式6段第二层及以下给下级
// 替换 hmacServer 为自己的地址
authStr = authParts[0] + "|" + authParts[1] + "|" + authParts[2] + "|" + authParts[3] + "|" + hmacServer + "|" + authParts[5];
if (!heartbeat)
Mprintf("V1 转发 Authorization: %s. AuthServer: %s\n", sn.c_str(), hmacServer.c_str());
}
return authStr.empty() ? "" : TcpClient::ObfuscateAuthorization(authStr);
}
void CPwdGenDlg::OnBnClickedButtonGenkey()
{
UpdateData(TRUE);
CString strBeginDate = m_StartTm.FormatL("%Y%m%d");
CString strEndDate = m_ExpireTm.FormatL("%Y%m%d");
CString hostNum;
hostNum.FormatL("%04d", m_nHostNum);
std::string fixedKey;
std::string pwdHmacStr; // V1: 数字字符串, V2: "v2:..." 签名
std::string hmacV1; // 只有 V1 使用
if (m_nVersion == 0) {
// V1 (HMAC) 模式
if (m_sUserPwd.IsEmpty()) return;
std::string pwdHash = hashSHA256(m_sUserPwd.GetString());
if (pwdHash != GetPwdHash()) {
Mprintf("hashSHA256 [%s]: %s\n", m_sUserPwd, pwdHash.c_str());
MessageBoxL("您输入的密码不正确,无法生成口令!", "提示", MB_OK | MB_ICONWARNING);
m_sUserPwd.Empty();
return;
}
// 密码形式20250209 - 20350209: SHA256: HostNum
std::string password = std::string(strBeginDate.GetString()) + " - " + strEndDate.GetBuffer() + ": " + GetPwdHash() + ": " + hostNum.GetBuffer();
std::string finalKey = deriveKey(password, m_sDeviceID.GetString());
fixedKey = strBeginDate.GetString() + std::string("-") + strEndDate.GetBuffer() + std::string("-") + hostNum.GetString() + "-" +
getFixedLengthID(finalKey);
m_sPassword = fixedKey.c_str();
m_EditPassword.SetWindowTextA(fixedKey.c_str());
hmacV1 = genHMAC(pwdHash, m_sUserPwd.GetString());
uint64_t pwdHmac = SignMessage(m_sUserPwd.GetString(), (BYTE*)fixedKey.c_str(), fixedKey.length());
pwdHmacStr = std::to_string(pwdHmac);
m_sHMAC = pwdHmacStr.c_str();
m_EditHMAC.SetWindowTextA(m_sHMAC);
// 多层授权:检查是否有 Authorization + master公网地址
std::string fullAuth = BuildV1Authorization(m_sDeviceID.GetString());
if (!fullAuth.empty()) {
m_sAuthorization = fullAuth.c_str();
m_EditAuthorization.SetWindowText(m_sAuthorization);
}
else {
m_sAuthorization.Empty();
m_EditAuthorization.SetWindowText(_T(""));
}
} else {
// V2 (ECDSA) 模式
if (m_sPrivateKeyPath.IsEmpty()) {
MessageBoxL("请选择私钥文件!", "提示", MB_OK | MB_ICONWARNING);
return;
}
// 检查私钥文件是否存在
if (GetFileAttributes(m_sPrivateKeyPath) == INVALID_FILE_ATTRIBUTES) {
MessageBoxL("私钥文件不存在!", "错误", MB_OK | MB_ICONERROR);
return;
}
// V2 口令格式与 V1 相同,但不需要密码验证
// 使用 deviceId + dates 作为种子生成 key
std::string seed = m_sDeviceID.GetString() + std::string(strBeginDate.GetString()) +
strEndDate.GetString() + hostNum.GetString();
std::string seedHash = hashSHA256(seed);
fixedKey = strBeginDate.GetString() + std::string("-") + strEndDate.GetBuffer() +
std::string("-") + hostNum.GetString() + "-" + getFixedLengthID(seedHash);
// 使用私钥签名
pwdHmacStr = signPasswordV2(m_sDeviceID.GetString(), fixedKey, m_sPrivateKeyPath.GetString());
if (pwdHmacStr.empty()) {
MessageBoxL("生成 V2 签名失败!\n请检查私钥文件是否有效。", "错误", MB_OK | MB_ICONERROR);
return;
}
m_sPassword = fixedKey.c_str();
m_EditPassword.SetWindowTextA(fixedKey.c_str());
m_sHMAC = pwdHmacStr.c_str();
m_EditHMAC.SetWindowTextA(pwdHmacStr.c_str());
// V2 模式:生成 Authorization使用 deviceID 计算 snHashPrefix
// Authorization 的并发数限制:使用下级并发数(已自动同步或手动设置)
CString authHostNum;
authHostNum.Format(_T("%04d"), m_nAuthHostNum);
// 从 fixedKey 提取 license: "20260317-20270317-0256-..." → "20260317|20270317|0256"
std::string license = strBeginDate.GetString() + std::string("|") +
strEndDate.GetString() + "|" + authHostNum.GetString();
std::string snHashPrefix = computeSnHashPrefix(m_sDeviceID.GetString());
std::string authSig = signAuthorizationV2(license, snHashPrefix, m_sPrivateKeyPath.GetString());
if (!authSig.empty()) {
// Authorization 格式V25段: startDate|endDate|hostNum|snHashPrefix|signature
std::string auth = license + "|" + snHashPrefix + "|" + authSig;
m_sAuthorization = TcpClient::ObfuscateAuthorization(auth).c_str(); // 混淆后显示
m_EditAuthorization.SetWindowText(m_sAuthorization);
GetDlgItem(IDC_STATIC_AUTHORIZATION)->ShowWindow(SW_SHOW);
GetDlgItem(IDC_EDIT_AUTHORIZATION)->ShowWindow(SW_SHOW);
Mprintf("V2 生成 Authorization: %s (snHashPrefix=%s, authHostNum=%s)\n",
m_sDeviceID.GetString(), snHashPrefix.c_str(), authHostNum.GetString());
} else {
m_sAuthorization.Empty();
m_EditAuthorization.SetWindowText(_T(""));
}
}
// 公共部分:判断是否为本机授权
std::string hardwareID = CMy2015RemoteDlg::GetHardwareID();
std::string hashedID = hashSHA256(hardwareID);
std::string deviceID = getFixedLengthID(hashedID);
m_bIsLocalDevice = (deviceID == m_sDeviceID.GetString());
bool isSuperAdmin = GetUpperHash()==GetPwdHash();
if (m_bIsLocalDevice && isSuperAdmin) {
// 给自己授权,自动保存
auto settings = "settings", pwdKey = "Password";
THIS_CFG.SetStr(settings, pwdKey, fixedKey.c_str());
THIS_CFG.SetStr("settings", "SN", deviceID);
THIS_CFG.SetStr(settings, "HMAC", hmacV1);
THIS_CFG.SetStr(settings, "PwdHmac", pwdHmacStr);
m_BtnSaveLicense.EnableWindow(FALSE);
} else if (m_bIsLocalDevice && !isSuperAdmin) {
// 没有权限给自己生成授权
MessageBoxL("您无法给自己授权!\n生成的授权信息仅供参考,不会自动保存。",
"提示", MB_OK | MB_ICONWARNING);
m_BtnSaveLicense.EnableWindow(FALSE);
} else {
m_BtnSaveLicense.EnableWindow(TRUE);
}
}
BOOL CPwdGenDlg::OnInitDialog()
{
__super::OnInitDialog();
// 设置对话框标题和控件文本(解决英语系统乱码问题)
SetWindowText(_TR("生成口令"));
SetDlgItemText(IDC_BUTTON_SAVE_LICENSE, _TR("保存授权"));
SetDlgItemText(IDCANCEL, _TR("取消"));
SetDlgItemText(IDC_STATIC_PWD_LABEL, _TR("密 码:"));
SetDlgItemText(IDC_STATIC_KEYGEN_VERSION, _TR("版 本:"));
SetDlgItemText(IDC_STATIC_KEYGEN_SERIAL, _TR("序列号:"));
SetDlgItemText(IDC_STATIC_KEYGEN_EXPIRE, _TR("有效期:"));
SetDlgItemText(IDC_STATIC_KEYGEN_CONN, _TR("连接数:"));
SetDlgItemText(IDC_STATIC_AUTH_HOSTNUM, _TR("下级连接数:"));
SetDlgItemText(IDC_STATIC_KEYGEN_TOKEN, _TR("口 令:"));
SetDlgItemText(IDC_STATIC_KEYGEN_HMAC_2373, _TR("HMAC:"));
SetDlgItemText(IDC_BUTTON_GENKEY, _TR("生成"));
GetDlgItem(IDC_STATIC_KEYGEN_VERSION)->EnableWindow(GetUpperHash() == GetPwdHash());
// TODO: 在此添加额外的初始化
m_hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_PASSWORD));
SetIcon(m_hIcon, FALSE);
// 初始化版本下拉框
m_ComboVersion.InsertString(0, _T("V1 (HMAC)"));
m_ComboVersion.InsertString(1, _T("V2 (ECDSA)"));
m_ComboVersion.SetCurSel(0);
m_ComboVersion.EnableWindow(GetUpperHash() == GetPwdHash());
m_nVersion = 0;
// 初始状态V1 模式,隐藏私钥控件和下级并发数
m_EditPrivateKey.ShowWindow(SW_HIDE);
m_BtnBrowseKey.ShowWindow(SW_HIDE);
m_BtnGenKeyPair.ShowWindow(SW_HIDE);
m_EditUserPwd.ShowWindow(SW_SHOW);
// 隐藏下级并发数控件(仅 V2 模式使用)
GetDlgItem(IDC_STATIC_AUTH_HOSTNUM)->ShowWindow(SW_HIDE);
m_EditAuthHostNum.ShowWindow(SW_HIDE);
// 初始状态禁用保存按钮
m_BtnSaveLicense.EnableWindow(FALSE);
// 加载已配置的 V2 私钥路径
std::string v2Key = THIS_CFG.GetStr("settings", "V2PrivateKey", "");
if (!v2Key.empty()) {
m_sPrivateKeyPath = v2Key.c_str();
m_EditPrivateKey.SetWindowText(m_sPrivateKeyPath);
}
// 设置根凭证标签翻译
SetDlgItemText(IDC_STATIC_AUTHORIZATION, _TR("根凭证:"));
// 检查并显示当前服务端的 Authorization
std::string storedAuth = THIS_CFG.GetStr("settings", "Authorization", "");
if (!storedAuth.empty()) {
// 服务端已获得上级授权,在标题栏提示
CString title;
title.Format(_T("%s - %s"), _TR("生成口令"), _TR("已获得上级授权"));
SetWindowText(title);
}
// 初始化 FRP 代理控件
SetDlgItemText(IDC_CHECK_FRP_PROXY, _TR("提供 FRP 代理"));
SetDlgItemText(IDC_STATIC_FRP_REMOTE_PORT, _TR("远程端口:"));
SetDlgItemText(IDC_BTN_FRP_AUTO_PORT, _TR("自动"));
m_nFrpRemotePort = 0;
m_sFrpConfig.clear();
// 检查 FRPS 是否已配置
if (CFrpsForSubDlg::IsFrpsConfigured()) {
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
CString frpsInfo;
frpsInfo.Format(_T("FRPS: %s:%d"), CString(frpsConfig.server.c_str()), frpsConfig.port);
m_StaticFrpInfo.SetWindowText(frpsInfo);
m_CheckFrpProxy.EnableWindow(TRUE);
} else {
m_StaticFrpInfo.SetWindowText(_TR("(未配置 FRPS请先通过 扩展→下级FRP代理设置)"));
m_CheckFrpProxy.EnableWindow(FALSE);
}
m_CheckFrpProxy.SetCheck(BST_UNCHECKED);
UpdateFrpControlStates();
return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}
void CPwdGenDlg::OnCbnSelchangeComboVersion()
{
m_nVersion = m_ComboVersion.GetCurSel();
// 切换版本时清理已生成的值
m_EditPassword.SetWindowText(_T(""));
m_EditHMAC.SetWindowText(_T(""));
m_EditAuthorization.SetWindowText(_T(""));
m_sPassword.Empty();
m_sHMAC.Empty();
m_sAuthorization.Empty();
m_BtnSaveLicense.EnableWindow(FALSE);
// 重置下级并发数手动标志,并同步当前连接数
m_bAuthHostNumManual = FALSE;
CString strHostNum;
m_EditHostNum.GetWindowText(strHostNum);
int hostNum = _ttoi(strHostNum);
if (hostNum >= 2) {
m_nAuthHostNum = hostNum;
CString strAuthHostNum;
strAuthHostNum.Format(_T("%d"), m_nAuthHostNum);
m_EditAuthHostNum.SetWindowText(strAuthHostNum);
}
// 获取"密码"标签控件和"授权"控件
CWnd* pLabel = GetDlgItem(IDC_STATIC_PWD_LABEL);
CWnd* pAuthLabel = GetDlgItem(IDC_STATIC_AUTHORIZATION);
CWnd* pAuthHostNumLabel = GetDlgItem(IDC_STATIC_AUTH_HOSTNUM);
if (m_nVersion == 0) {
// V1 (HMAC) 模式:显示密码输入框,隐藏私钥相关控件
m_EditUserPwd.ShowWindow(SW_SHOW);
m_EditPrivateKey.ShowWindow(SW_HIDE);
m_BtnBrowseKey.ShowWindow(SW_HIDE);
m_BtnGenKeyPair.ShowWindow(SW_HIDE);
if (pLabel) pLabel->SetWindowText(_TR("密 码:"));
// V1 模式隐藏下级并发数
if (pAuthHostNumLabel) pAuthHostNumLabel->ShowWindow(SW_HIDE);
m_EditAuthHostNum.ShowWindow(SW_HIDE);
} else {
// V2 (ECDSA) 模式:隐藏密码输入框,显示私钥相关控件
m_EditUserPwd.ShowWindow(SW_HIDE);
m_EditPrivateKey.ShowWindow(SW_SHOW);
m_BtnBrowseKey.ShowWindow(SW_SHOW);
m_BtnGenKeyPair.ShowWindow(SW_SHOW);
if (pLabel) pLabel->SetWindowText(_TR("私 钥:"));
// V2 模式显示下级并发数
if (pAuthHostNumLabel) pAuthHostNumLabel->ShowWindow(SW_SHOW);
m_EditAuthHostNum.ShowWindow(SW_SHOW);
// 如果已有有效私钥,禁用浏览和生成按钮,私钥路径只读
std::string v2Key = THIS_CFG.GetStr("settings", "V2PrivateKey", "");
bool hasValidKey = !v2Key.empty() && GetFileAttributes(v2Key.c_str()) != INVALID_FILE_ATTRIBUTES;
m_BtnBrowseKey.EnableWindow(!hasValidKey);
m_BtnGenKeyPair.EnableWindow(!hasValidKey);
m_EditPrivateKey.SetReadOnly(hasValidKey);
}
}
void CPwdGenDlg::OnEnChangeEditHostNum()
{
// 如果下级并发数未手动修改,则跟随连接数变化
if (!m_bAuthHostNumManual && m_nVersion == 1) { // 仅 V2 模式需要同步
CString strHostNum;
m_EditHostNum.GetWindowText(strHostNum);
int hostNum = _ttoi(strHostNum);
if (hostNum >= 2) {
m_nAuthHostNum = hostNum;
CString strAuthHostNum;
strAuthHostNum.Format(_T("%d"), m_nAuthHostNum);
m_EditAuthHostNum.SetWindowText(strAuthHostNum);
}
}
}
void CPwdGenDlg::OnEnChangeEditAuthHostNum()
{
// 检测是否为用户手动修改(而非程序同步)
// 通过比较当前值与连接数来判断
CString strAuthHostNum;
m_EditAuthHostNum.GetWindowText(strAuthHostNum);
CString strHostNum;
m_EditHostNum.GetWindowText(strHostNum);
// 如果两个值不同,说明用户手动修改了
if (strAuthHostNum != strHostNum && !strAuthHostNum.IsEmpty()) {
m_bAuthHostNumManual = TRUE;
}
}
void CPwdGenDlg::OnBnClickedButtonBrowseKey()
{
// 检查是否有权使用 V2 私钥
if (GetUpperHash() != GetPwdHash()) {
MessageBoxL("您无法使用 V2 私钥签名!", "提示", MB_OK | MB_ICONWARNING);
return;
}
CFileDialog dlg(TRUE, _T("key"), nullptr,
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
_T("Key Files (*.key;*.pem)|*.key;*.pem|All Files (*.*)|*.*||"),
this);
dlg.m_ofn.lpstrTitle = _T("Select Private Key");
if (dlg.DoModal() == IDOK) {
m_sPrivateKeyPath = dlg.GetPathName();
m_EditPrivateKey.SetWindowText(m_sPrivateKeyPath);
}
}
void CPwdGenDlg::OnBnClickedButtonGenKeypair()
{
// 检查是否有权生成 V2 密钥对
if (GetUpperHash() != GetPwdHash()) {
MessageBoxL("您无法生成 V2 密钥对!", "提示", MB_OK | MB_ICONWARNING);
return;
}
// 选择私钥保存位置
CFileDialog dlg(FALSE, _T("key"), _T("private_key.key"),
OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
_T("Key Files (*.key)|*.key|All Files (*.*)|*.*||"),
this);
dlg.m_ofn.lpstrTitle = _T("Save Private Key");
if (dlg.DoModal() != IDOK) {
return;
}
CString privateKeyPath = dlg.GetPathName();
// 生成密钥对
BYTE publicKey[V2_PUBKEY_SIZE];
if (!GenerateKeyPairV2(privateKeyPath.GetString(), publicKey)) {
MessageBoxL("生成密钥对失败!", "错误", MB_OK | MB_ICONERROR);
return;
}
// 更新私钥路径(仅更新界面,不保存到配置,需通过菜单设置)
m_sPrivateKeyPath = privateKeyPath;
m_EditPrivateKey.SetWindowText(m_sPrivateKeyPath);
// 格式化公钥为 C 代码
std::string pubKeyCode = formatPublicKeyAsCode(publicKey);
// 生成公钥文件路径(与私钥同目录,扩展名改为 .pub.h
CString publicKeyPath = privateKeyPath;
int dotPos = publicKeyPath.ReverseFind('.');
if (dotPos > 0) {
publicKeyPath = publicKeyPath.Left(dotPos) + _T(".pub.h");
} else {
publicKeyPath += _T(".pub.h");
}
// 保存公钥到文件
FILE* fp = nullptr;
if (fopen_s(&fp, publicKeyPath.GetString(), "w") == 0 && fp) {
fprintf(fp, "// V2 License Public Key\n");
fprintf(fp, "// Generated: %s\n\n", CTime::GetCurrentTime().Format("%Y-%m-%d %H:%M:%S").GetString());
fprintf(fp, "%s\n", pubKeyCode.c_str());
fclose(fp);
}
// 复制公钥到剪贴板
if (OpenClipboard()) {
EmptyClipboard();
size_t len = pubKeyCode.length() + 1;
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
if (hMem) {
char* pMem = (char*)GlobalLock(hMem);
if (pMem) {
memcpy(pMem, pubKeyCode.c_str(), len);
GlobalUnlock(hMem);
SetClipboardData(CF_TEXT, hMem);
}
}
CloseClipboard();
}
// 简短提示
CString msg;
msg.Format(_T("密钥对生成成功!\n\n私钥: %s\n公钥: %s\n\n公钥代码已复制到剪贴板"),
privateKeyPath.GetString(), publicKeyPath.GetString());
MessageBox(msg, _TR("成功"), MB_OK | MB_ICONINFORMATION);
}
void CPwdGenDlg::OnBnClickedCheckFrpProxy()
{
UpdateFrpControlStates();
// 勾选时,如果端口为 0自动填入下一个可用端口
if (m_CheckFrpProxy.GetCheck() == BST_CHECKED) {
CString portText;
m_EditFrpRemotePort.GetWindowText(portText);
int currentPort = _ttoi(portText);
if (currentPort <= 0) {
int port = CFrpsForSubDlg::FindNextAvailablePort();
if (port > 0) {
CString portStr;
portStr.Format(_T("%d"), port);
m_EditFrpRemotePort.SetWindowText(portStr);
}
}
}
}
void CPwdGenDlg::OnBnClickedBtnFrpAutoPort()
{
// 自动查找下一个可用端口(不保存,等保存授权时再记录)
int port = CFrpsForSubDlg::FindNextAvailablePort();
if (port > 0) {
m_nFrpRemotePort = port;
CString portStr;
portStr.Format(_T("%d"), port);
m_EditFrpRemotePort.SetWindowText(portStr);
} else {
MessageBoxL("端口范围已满,无法自动分配!", "提示", MB_OK | MB_ICONWARNING);
}
}
void CPwdGenDlg::UpdateFrpControlStates()
{
BOOL frpEnabled = (m_CheckFrpProxy.GetCheck() == BST_CHECKED);
BOOL frpsConfigured = CFrpsForSubDlg::IsFrpsConfigured();
m_EditFrpRemotePort.EnableWindow(frpEnabled && frpsConfigured);
m_BtnFrpAutoPort.EnableWindow(frpEnabled && frpsConfigured);
// 如果启用 FRP 且端口为空,自动分配一个
if (frpEnabled && frpsConfigured && m_nFrpRemotePort == 0) {
CString portText;
m_EditFrpRemotePort.GetWindowText(portText);
if (portText.IsEmpty() || _ttoi(portText) == 0) {
// 不自动分配,让用户点击"自动"或手动输入
}
}
}
void CPwdGenDlg::OnBnClickedButtonSaveLicense()
{
UpdateData(TRUE);
// 验证数据
if (m_sDeviceID.IsEmpty() || m_sPassword.IsEmpty() || m_sHMAC.IsEmpty()) {
MessageBoxL("请先生成口令!", "提示", MB_OK | MB_ICONWARNING);
return;
}
// 提示用户输入备注(可选)
CString remark;
// 这里可以弹出一个简单的输入对话框获取备注,或者直接保存
// 检查是否启用了 FRP 代理
std::string frpConfig;
if (m_CheckFrpProxy.GetCheck() == BST_CHECKED && CFrpsForSubDlg::IsFrpsConfigured()) {
// 获取远程端口
CString portText;
m_EditFrpRemotePort.GetWindowText(portText);
int remotePort = _ttoi(portText);
if (remotePort < 1024 || remotePort > 65535) {
MessageBoxL("FRP 远程端口无效1024-65535", "提示", MB_OK | MB_ICONWARNING);
m_EditFrpRemotePort.SetFocus();
return;
}
// 获取 FRPS 配置
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
// 生成 FRP 配置(有效期设为最大值,由 License 控制过期)
// authMode: 0 = 官方 FRP (token), 1 = 自定义 FRP (privilegeKey)
// 注意expireDate 最大 20371231因为自定义 FRP 的 timestamp 参数是 32 位 long2038 年后会溢出
frpConfig = GenerateFrpConfig(frpsConfig.server, frpsConfig.port, remotePort, frpsConfig.token, "20371231", frpsConfig.authMode);
if (!frpConfig.empty()) {
// 记录端口分配
CFrpsForSubDlg::RecordPortAllocation(remotePort, m_sDeviceID.GetString());
Mprintf("[FRP] 为序列号 %s 分配端口 %d\n", m_sDeviceID.GetString(), remotePort);
}
}
// 保存授权信息到数据库
bool success = SaveLicenseInfo(
m_sDeviceID.GetString(),
m_sPassword.GetString(),
m_sHMAC.GetString(),
remark.GetString(),
m_sAuthorization.GetString(),
frpConfig
);
if (!success) {
MessageBoxL("保存授权信息失败!", "错误", MB_OK | MB_ICONERROR);
return;
}
// 询问是否导出文件
if (MessageBoxL("授权已保存到数据库。\n\n是否同时导出为文件?\n导出后可发送给目标设备导入使用。",
"导出授权", MB_YESNO | MB_ICONQUESTION) == IDYES) {
// 构建默认文件名(使用完整 SN
CString defaultName;
defaultName.Format(_T(BRAND_LICENSE_PREFIX "_%s.lic"), m_sDeviceID.GetString());
// 弹出文件保存对话框
CString filter = _T(BRAND_LICENSE_DESC " (*.lic)|*.lic|All Files (*.*)|*.*||");
CString dlgTitle = _TR("保存授权文件");
CFileDialog dlg(FALSE, _T("lic"), defaultName,
OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY, filter, this);
dlg.m_ofn.lpstrTitle = dlgTitle;
if (dlg.DoModal() == IDOK) {
// 导出文件
if (ExportLicenseFile(
std::string(CT2A(dlg.GetPathName().GetString())),
std::string(CT2A(m_sDeviceID.GetString())),
std::string(CT2A(m_sPassword.GetString())),
std::string(CT2A(m_sHMAC.GetString())),
std::string(CT2A(m_sAuthorization.GetString())),
frpConfig)) {
CString msg;
msg.Format(_T("%s\n%s"), _TR("授权文件已导出到:"), dlg.GetPathName().GetString());
MessageBox(msg, _TR("导出成功"), MB_OK | MB_ICONINFORMATION);
} else {
MessageBoxL("无法写入文件", "导出失败", MB_OK | MB_ICONERROR);
}
}
} else {
// 只保存到数据库,显示成功信息
CString msg;
if (m_sAuthorization.IsEmpty()) {
msg.FormatL("授权信息已保存!\n\n序列号: %s\n口令: %s\nHMAC: %s\n\n存储位置: %s",
m_sDeviceID, m_sPassword, m_sHMAC, GetLicensesPath().c_str());
} else {
msg.FormatL("授权信息已保存!\n\n序列号: %s\n口令: %s\nHMAC: %s\nAuthorization: %s\n\n存储位置: %s",
m_sDeviceID, m_sPassword, m_sHMAC, m_sAuthorization, GetLicensesPath().c_str());
}
MessageBox(msg, _TR("保存成功"), MB_OK | MB_ICONINFORMATION);
}
}