Feat: TLS certificate menu and config dialog for the MFC server

Add a "TLS证书" item to the Extensions menu below "MCP设置" that opens a
runtime-built dialog to enable one-way TLS and set the certificate and
private-key paths. On OK it writes the [tls] Enabled/CertPath/KeyPath keys
and calls ServerTlsIdentity::Load() so the change takes effect for new
connections immediately (design doc §7.1), mirroring the existing
CMcpSettingsDlg runtime-template pattern. All new strings are wired into
the en_US / zh_TW / de_DE language files, and the menu item is gated by
HIDE_MENU_TLS_CERTIFICATE / MF_TLS_CERTIFICATE like MCP settings.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-15 19:42:52 +02:00
1 parent 96b9eea70a
commit c93f09e9ac
12 files changed
+351 -2

No files matched your search

+234
View File
@@ -0,0 +1,234 @@
#include "stdafx.h"
#include "TlsCertificateDlg.h"
#include "LangManager.h" // _TR / MessageBoxL
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
#include "ServerTlsSession.h" // ServerTlsIdentity::Load / IsLoadError
#include "common/iniFile.h" // config::GetInt/GetStr/SetInt/SetStr
#include <atlconv.h>
#include <afxdlgs.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
namespace {
// 构造一个无控件的 DLGTEMPLATE:cdit=0,控件由 OnInitDialog 动态创建。
// (与 McpSettingsDlg.cpp / ZstaPickerDlg.cpp 内同名函数一致)
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption, short cx, short cy)
{
out.clear();
auto append = [&](const void* p, size_t n) {
const BYTE* b = (const BYTE*)p;
out.insert(out.end(), b, b + n);
};
auto appendW = [&](WORD v) {
out.push_back((BYTE)(v & 0xFF));
out.push_back((BYTE)((v >> 8) & 0xFF));
};
auto appendWStr = [&](LPCWSTR s) {
size_t n = wcslen(s);
append(s, (n + 1) * sizeof(WCHAR));
};
DLGTEMPLATE dt = { 0 };
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
WS_POPUP | WS_CAPTION | WS_SYSMENU;
dt.dwExtendedStyle = 0;
dt.cdit = 0;
dt.x = 0; dt.y = 0;
dt.cx = cx; dt.cy = cy;
append(&dt, sizeof(dt));
appendW(0); // no menu
appendW(0); // default dialog class
appendWStr(caption); // caption
appendW(8); // font point size
appendWStr(L"MS Shell Dlg"); // typeface
while (out.size() % 4) out.push_back(0); // DWORD align
}
} // namespace
BEGIN_MESSAGE_MAP(CTlsCertificateDlg, CDialog)
ON_BN_CLICKED(IDC_TLS_CERT_BROWSE, &CTlsCertificateDlg::OnBrowseCert)
ON_BN_CLICKED(IDC_TLS_KEY_BROWSE, &CTlsCertificateDlg::OnBrowseKey)
END_MESSAGE_MAP()
CTlsCertificateDlg::CTlsCertificateDlg(CWnd* parent)
: CDialog((LPCTSTR)NULL, parent)
{
}
INT_PTR CTlsCertificateDlg::DoModal()
{
USES_CONVERSION;
CString title = _TR("TLS证书");
BuildDialogTemplate(m_Template, T2CW(title), 340, 180);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
BOOL CTlsCertificateDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CRect cli;
GetClientRect(&cli);
// 占位 rect,真正布局在 LayoutControls 里
CRect r0(0, 0, 10, 10);
m_btnEnable.Create(_TR("启用加密"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_TLS_ENABLE);
m_lblCert.Create(_TR("证书路径"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editCert.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_TLS_CERT);
m_btnCertBrowse.Create(_TR("浏览"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
r0, this, IDC_TLS_CERT_BROWSE);
m_lblKey.Create(_TR("私钥路径"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editKey.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_TLS_KEY);
m_btnKeyBrowse.Create(_TR("浏览"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
r0, this, IDC_TLS_KEY_BROWSE);
m_btnOK.Create(_TR("确定"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
r0, this, IDOK);
m_btnCancel.Create(_TR("取消"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
r0, this, IDCANCEL);
// 子控件继承对话框字体 (DS_SETFONT)
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
if (hFont) {
m_btnEnable.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblCert.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editCert.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnCertBrowse.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblKey.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editKey.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnKeyBrowse.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
// 回填配置
int enabled = THIS_CFG.GetInt("tls", "Enabled", 0);
std::string cert = THIS_CFG.GetStr("tls", "CertPath", "");
std::string key = THIS_CFG.GetStr("tls", "KeyPath", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
m_editCert.SetWindowText(CString(cert.c_str()));
m_editKey.SetWindowText(CString(key.c_str()));
LayoutControls(cli.Width(), cli.Height());
return TRUE;
}
void CTlsCertificateDlg::OnOK()
{
CString sCert, sKey;
m_editCert.GetWindowText(sCert);
m_editKey.GetWindowText(sKey);
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
std::string cert = CT2A(sCert);
std::string key = CT2A(sKey);
// 校验:启用加密时必须填写证书/私钥路径
if (enabled && cert.empty()) {
MessageBoxL("请填写证书路径", "提示", MB_ICONWARNING);
return;
}
if (enabled && key.empty()) {
MessageBoxL("请填写私钥路径", "提示", MB_ICONWARNING);
return;
}
// 落盘 [tls] 三键
THIS_CFG.SetInt("tls", "Enabled", enabled ? 1 : 0);
THIS_CFG.SetStr("tls", "CertPath", cert);
THIS_CFG.SetStr("tls", "KeyPath", key);
// 热重载身份(设计文档 §7.1):开关与证书/私钥路径对新建连接即时生效。
// 已知窄竞态:Load 内部 FreeIdentity 与「已取 Cred 副本、尚在握手」的连接存在竞态,
// 未来热切换需引用计数 + 惰性释放(见 ServerTlsSession.cpp FreeIdentity 注释),本次不做。
ServerTlsIdentity::Load(THIS_CFG);
if (ServerTlsIdentity::IsLoadError()) {
MessageBoxL("证书/私钥加载失败,加密功能未生效。请检查文件路径与格式(证书 PEM、私钥 PKCS#8)。",
"提示", MB_ICONWARNING);
} else {
MessageBox(_TR("TLS 配置已保存。") + _T("\r\n") +
_TR("改动对新建连接即时生效。"),
_TR("提示"), MB_ICONINFORMATION);
}
CDialog::OnOK();
}
void CTlsCertificateDlg::OnBrowseCert()
{
CFileDialog dlg(TRUE, _T("crt"), nullptr,
OFN_FILEMUSTEXIST | OFN_HIDEREADONLY,
_T("Certificate Files (*.crt;*.pem;*.cer;*.cert)|*.crt;*.pem;*.cer;*.cert|All Files (*.*)|*.*||"),
this);
dlg.m_ofn.lpstrTitle = _T("Select Certificate");
if (dlg.DoModal() == IDOK) {
m_editCert.SetWindowText(dlg.GetPathName());
}
}
void CTlsCertificateDlg::OnBrowseKey()
{
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_editKey.SetWindowText(dlg.GetPathName());
}
}
void CTlsCertificateDlg::LayoutControls(int cx, int cy)
{
const int margin = 14;
const int labelW = 72;
const int rowH = 24;
const int gap = 10;
const int browseW = 64;
const int btnW = 88;
const int btnH = 26;
int y = margin;
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
int editW = cx - margin * 2 - labelW - browseW - gap;
m_lblCert.MoveWindow(margin, y, labelW, rowH);
m_editCert.MoveWindow(margin + labelW, y - 2, editW, rowH);
m_btnCertBrowse.MoveWindow(cx - margin - browseW, y - 2, browseW, rowH);
y += rowH + gap;
m_lblKey.MoveWindow(margin, y, labelW, rowH);
m_editKey.MoveWindow(margin + labelW, y - 2, editW, rowH);
m_btnKeyBrowse.MoveWindow(cx - margin - browseW, y - 2, browseW, rowH);
y += rowH + gap;
// 确定 / 取消按钮紧随私钥路径下方
m_btnCancel.MoveWindow(cx - margin - btnW, y, btnW, btnH);
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, y, btnW, btnH);
y += btnH + margin;
// 收缩对话框到内容实际高度并重新居中,消除底部空白。
if (y < cy) {
CRect wnd;
GetWindowRect(&wnd);
SetWindowPos(NULL, 0, 0, wnd.Width(), y + (wnd.Height() - cy),
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
CenterWindow();
}
}