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
This commit is contained in:
yuanyuanxiang
2026-08-30 13:19:51 +02:00
parent ac2855198b
commit 5c77f5ce14
6 changed files with 875 additions and 2 deletions

View File

@@ -2201,6 +2201,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);
// 远程控制开关默认关要求只读关McpReadonly=0才生效工具列表/分派双重门控)。
McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings", "McpRemoteControl", 0) != 0);
// 文件传输开关:默认关;仅 download_file 门控(不要求只读关)。
McpServer().SetFileTransferEnabled(THIS_CFG.GetInt("settings", "McpFileTransfer", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
@@ -5843,6 +5845,14 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
// V2 文件传输(支持 C2C
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
// P6MCP download_file 挂起时接管流式子连接dstClientID==0 → 主控端)。
// OnFileChunkV2 自身会做 len 校验,这里先判长度再读 dstClientID避免越界。
if (len >= sizeof(FileChunkPacketV2) && pkt->dstClientID == 0 &&
McpServer().IsFileTransferPending(ContextObject->GetClientID())) {
McpServer().OnFileChunkV2(ContextObject->GetClientID(), ContextObject, szBuffer, len);
break;
}
if (pkt->dstClientID == 0) {
// 目标是主控端:本地接收
if (ContextObject->hDlg == NULL) {
@@ -6103,6 +6113,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
if (len < sizeof(FileCompletePacketV2)) break;
FileCompletePacketV2* pkt = (FileCompletePacketV2*)szBuffer;
// P6MCP download_file 挂起时接管完成校验包dstClientID==0 → 主控端)。
if (pkt->dstClientID == 0 &&
McpServer().IsFileTransferPending(ContextObject->GetClientID())) {
McpServer().OnFileCompleteV2(ContextObject->GetClientID(), szBuffer, len);
break;
}
if (pkt->dstClientID == 0) {
// 目标是主控端:本地校验
bool verifyOk = HandleFileCompleteV2((char*)szBuffer, len, 0);
@@ -6436,6 +6453,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
ContextObject->CancelIO(); // 只列盘 → 用完即关一次性子链接
break;
}
// P6MCP download_file 挂起时接管文件管理器子链接,下发 CMD_DOWN_FILES_V2。
// true=已接管保持子链接ClearFileTransfer 收尾false=会话已清理(迟到包)→ 关孤儿子链接。
if (McpServer().IsFileTransferPending(devId)) {
if (!McpServer().OnDownloadDriveList(devId, ContextObject))
ContextObject->CancelIO();
break;
}
ContextObject->EnableZstdContext(6);
g_2015RemoteDlg->SendMessage(WM_OPENFILEMANAGERDIALOG, 0, (LPARAM)ContextObject);
break;