Files
SimpleRemoter/server/2015Remote/McpSettingsDlg.cpp
yuanyuanxiang 5c77f5ce14 Feature: Add download_file MCP tool (V2 protocol, SHA-256 verified)
download_file pulls a remote file or directory back to the controller over
the existing V2 file-transfer protocol. It reuses COMMAND_LIST_DRIVE to open
the file-manager sub-link, then sends CMD_DOWN_FILES_V2 so the Windows client
streams COMMAND_SEND_FILE_V2 chunks and a per-file COMMAND_FILE_COMPLETE_V2
SHA-256 checksum over its own authenticated sub-connection. The server routes
those two packet types into a new per-device FileTransferSession whenever a
download is pending, so the headless path never opens the GUI progress dialog;
the C2C and existing GUI branches are left untouched.

Files are written under a normalized local_dir, and every chunk filename is
resolved and verified to stay inside local_dir (directories included) to block
.. traversal; overwrite=false skips existing files and counts them as skipped.
One transfer per host (mutually exclusive with the one-shot pending registry),
a dedicated McpFileTransfer=0-by-default gate surfaced in the settings dialog,
and audit logging complete the change. upload_file remains future work.

Co-Authored-By: deepseek-v4-pro
2026-08-30 13:19:51 +02:00

301 lines
12 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.
#include "stdafx.h"
#include "McpSettingsDlg.h"
#include "LangManager.h" // _TR / MessageBoxL
#include "McpServer.h" // GenerateRandomToken
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
#include <atlconv.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
namespace {
// 构造一个无控件的 DLGTEMPLATEcdit=0控件由 OnInitDialog 动态创建。
// (与 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
}
// 把白名单文本规范化为「逗号分隔、逐项去首尾空白、去空项」的存储格式。
// 换行视作分隔符(与逗号等价),便于多行输入。
std::string NormalizeWhitelist(const std::string& raw)
{
std::string norm;
norm.reserve(raw.size());
for (char ch : raw)
norm += (ch == '\r' || ch == '\n') ? ',' : ch;
std::vector<std::string> items;
size_t start = 0;
while (start < norm.size()) {
size_t comma = norm.find(',', start);
std::string item = norm.substr(start, comma == std::string::npos ? std::string::npos : comma - start);
size_t b = item.find_first_not_of(" \t\r\n");
if (b != std::string::npos) {
size_t e = item.find_last_not_of(" \t\r\n");
item = item.substr(b, e - b + 1);
} else {
item.clear();
}
if (!item.empty()) items.push_back(item);
if (comma == std::string::npos) break;
start = comma + 1;
}
std::string out;
for (size_t i = 0; i < items.size(); ++i) {
if (i) out += ",";
out += items[i];
}
return out;
}
// 存储格式(逗号分隔)→ 多行展示(每行一条)。
std::string WhitelistForDisplay(const std::string& stored)
{
std::string disp;
disp.reserve(stored.size());
for (char ch : stored) {
if (ch == ',') disp += "\r\n";
else disp += ch;
}
return disp;
}
} // namespace
BEGIN_MESSAGE_MAP(CMcpSettingsDlg, CDialog)
END_MESSAGE_MAP()
CMcpSettingsDlg::CMcpSettingsDlg(CWnd* parent)
: CDialog((LPCTSTR)NULL, parent)
{
}
INT_PTR CMcpSettingsDlg::DoModal()
{
USES_CONVERSION;
CString title = _TR("MCP设置");
BuildDialogTemplate(m_Template, T2CW(title), 320, 420);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
BOOL CMcpSettingsDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CRect cli;
GetClientRect(&cli);
// 占位 rect真正布局在 LayoutControls 里
CRect r0(0, 0, 10, 10);
m_btnEnable.Create(_TR("启用 MCP"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_ENABLE);
m_lblPort.Create(_TR("端口"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editPort.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_PORT);
m_lblBind.Create(_TR("绑定地址"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editBind.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_BIND);
m_lblToken.Create(_TR("Token"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editToken.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_TOKEN);
m_btnReadonly.Create(_TR("只读模式(禁命令执行)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_READONLY);
m_btnTerminal.Create(_TR("启用持久终端(全命令,无白名单)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_TERMINAL);
m_btnRemoteControl.Create(_TR("启用远程控制AI 操控桌面)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_REMOTECONTROL);
m_btnFileTransfer.Create(_TR("启用文件传输download_file"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_FILETRANSFER);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
r0, this, IDC_MCP_WHITELIST);
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_lblPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnReadonly.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editWhitelist.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));
}
// 回填配置(首次打开且 token 为空时预填随机值,随保存持久化)
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
if (tok.empty()) tok = GenerateRandomToken();
int readonly = THIS_CFG.GetInt("settings", "McpReadonly", 1);
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
m_editPort.SetWindowText(CString(std::to_string(port).c_str()));
m_editBind.SetWindowText(CString(bind.c_str()));
m_editToken.SetWindowText(CString(tok.c_str()));
m_btnReadonly.SetCheck(readonly ? BST_CHECKED : BST_UNCHECKED);
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
LayoutControls(cli.Width(), cli.Height());
return TRUE;
}
void CMcpSettingsDlg::OnOK()
{
CString sPort, sBind, sToken, sWhitelist;
m_editPort.GetWindowText(sPort);
m_editBind.GetWindowText(sBind);
m_editToken.GetWindowText(sToken);
m_editWhitelist.GetWindowText(sWhitelist);
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
bool readonly = (m_btnReadonly.GetCheck() == BST_CHECKED);
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
// 端口校验1-65535
int port = atoi(CT2A(sPort));
if (port < 1 || port > 65535) {
MessageBoxL(_TR("端口需为 1-65535 的数字"), _TR("提示"), MB_ICONWARNING);
return;
}
std::string bind = CT2A(sBind);
if (bind.empty()) bind = "127.0.0.1";
std::string token = CT2A(sToken);
if (enabled && token.empty()) {
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
return;
}
// 落盘
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
THIS_CFG.SetInt("settings", "McpPort", port);
THIS_CFG.SetStr("settings", "McpBind", bind);
THIS_CFG.SetStr("settings", "McpToken", token);
THIS_CFG.SetInt("settings", "McpReadonly", readonly ? 1 : 0);
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端与远程控制仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK();
}
void CMcpSettingsDlg::LayoutControls(int cx, int cy)
{
const int margin = 14;
const int labelW = 72;
const int rowH = 24;
const int gap = 10;
const int btnW = 88;
const int btnH = 26;
int y = margin;
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_lblPort.MoveWindow(margin, y, labelW, rowH);
m_editPort.MoveWindow(margin + labelW, y - 2, 100, rowH);
y += rowH + gap;
m_lblBind.MoveWindow(margin, y, labelW, rowH);
m_editBind.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
y += rowH + gap;
m_lblToken.MoveWindow(margin, y, labelW, rowH);
m_editToken.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
y += rowH + gap;
m_btnReadonly.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnTerminal.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnRemoteControl.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
y += whitelistH + gap;
int bottomY = cy - margin - btnH;
m_btnCancel.MoveWindow(cx - margin - btnW, bottomY, btnW, btnH);
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, bottomY, btnW, btnH);
}