6 Commits
Author SHA1 Message Date
yuanyuanxiang 679136e73e Release v1.4.1 2026-10-20 15:00:35 +02:00
yuanyuanxiang b637c94e24 Feature: Allow user to select an installation location for client 2026-10-20 13:12:16 +02:00
yuanyuanxianganddeepseek-flash f76cd79bb0 Fix: Renew expired licenses from today and report the actual expiry
Preset renewal stored its target date as the old expiry plus the entered
days, while the authorization actually issued to the client is always
today plus a day count. For a license that had already expired, the
preset date was therefore in the past: SendPendingRenewal clamped the
day count to one, the client received a one-day license starting today,
but the renewal tip and the logs still printed the stale preset date.
The result was a message such as "renewed until 2026-05-30" while the
license really ran until tomorrow.

Presetting a renewal now bases the new date on today when the license
has already expired, and on the original expiry date otherwise, which
matches the one-day-per-day extension admins expect for live licenses.
The renewal dialog and the success prompt state which base was used, so
the computed date is never ambiguous. Renewal tips and both logging
paths (heartbeat-driven SendPendingRenewal and client-requested
GenerateRenewalInfo) now print the date that was actually issued, which
also corrects legacy preset records whose stored date has already
passed.

Co-Authored-By: deepseek-flash
2026-10-19 15:15:49 +02:00
yuanyuanxianganddeepseek-flash 39c8daa9b0 Fix: Keep the audit log open across WebSocket reconnects
Hiding the browser tab while the audit log was open closed the socket,
because the visibility handler treats only the remote screen, terminal
and wall pages as streaming pages. Returning to the tab reconnected the
socket, and the onopen restore path took its catch-all branch, which
navigated to the device list. The audit log therefore vanished whenever
the browser was minimized.

The catch-all branch now leaves the active page alone when it is the
audit log, and only refreshes the device list in the background. Every
other page keeps the previous behaviour: the login page advances with a
cached token, and the terminal page falls back to the device list
because its session died with the socket. Audit results already rendered
stay on screen, so the admin can keep reading without re-querying.

Co-Authored-By: deepseek-flash
2026-10-19 10:39:36 +02:00
yuanyuanxianganddeepseek-flash cd4f3675fd Improve: Replace native browser prompts with themed in-app dialogs
Native alert() and confirm() render their buttons in the browser or OS
locale, so on a German system prompts such as the Clear Screen Wall
confirmation showed German labels that disagreed with the rest of the
web UI. Both are now served by a single themed modal that reuses the
logout confirmation styling: an info variant with one OK button for
notices, and a danger variant with Cancel/OK for destructive actions.

The alert helper is fire-and-forget, while the confirm helper returns a
Promise, so the two call sites that need an answer (clearWall and
deleteUser) are now async. Dialog title and message are written with
textContent so device names, user names and server error strings can
never be interpreted as markup. A pending dialog is resolved as
cancelled when another one supersedes it, keeping callers from hanging,
and clicking the backdrop or pressing Escape closes the dialog. Remote
keyboard forwarding now ignores keys while a dialog is open.

Co-Authored-By: deepseek-flash
2026-10-19 10:27:09 +02:00
yuanyuanxianganddeepseek-flash bbb3d8c2fa Fix: Recover proxied client IPs and fit audit filters on phones
The MFC web server now resolves the real client address from the
WebSocket upgrade request when the TCP peer is loopback, so audit records
and login rate limiting stop reporting the reverse proxy's 127.0.0.1.
X-Forwarded-For is preferred over X-Real-IP because its last hop is the
address the local proxy appended; a client-supplied header list cannot
forge it. Forwarded headers are ignored for non-loopback peers so a
client connecting directly over the internet cannot spoof its identity
or rotate addresses to bypass throttling. FRP deployments can keep using
the existing Proxy Protocol v2 support instead.

Collapse the audit log search conditions behind a toolbar toggle that
starts closed and switches to an accent-colored "Hide filters" state
while the panel is open. On portrait phones the panel uses one control
per row, and From/To move their border and padding onto a wrapper so the
iOS WebKit datetime-local width miscalculation cannot overflow the
viewport.

Co-Authored-By: deepseek-flash
2026-10-19 10:05:58 +02:00
24 changed files with 537 additions and 138 deletions

No files matched your search

+25
View File
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志 ## 更新日志
### v1.4.1 (2026.10.20)
**Web 运维控制台 & 安全加固 & 单向 TLS**
**新功能:**
- **Web 屏幕墙**:同一标签页以 1×1 ~ 5×5 布局同时监控多台设备,桌面 9 路 / 触控 4 路实时解码,超出预算暂停并自动晋升;点击 tile 进入单画面控制;同一设备可重复添加并共享一路流;MFC 与 Go 服务端同步支持(旧前端 / 旧服务端自动降级)
- **安全审计日志**:登录 / 授权拒绝 / WS 生命周期 / 屏幕与终端会话 / 剪贴板(仅字节数)/ 设备认证 / 管理变更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 条内存环形缓冲;Web 新增审计日志查询页(时间 / 事件 / 操作者 / IP / 设备 / 关键字 / 告警过滤 + 分页)
- **离线设备历史(Go)**:`devices.json` 持久化所有登录过的设备,设备列表可按需显示离线记录;管理员可删除记录或对在线设备执行远程卸载
- **单向 TLS 传输**:设备端口同一端口嗅探 ClientHello,TLS 1.2 客户端加密、旧明文客户端不受影响;MFC「扩展 > TLS证书」运行时配置;在线列表新增 TLS 列,右键可远程启用 / 取消(客户端下次启动生效)
- **Web 远程桌面增强**:Ctrl+C/X/V 双向文本剪贴板同步、手机「I」输入框(经剪贴板桥注入)、登录会话跨标签页共享、管理员 SSO 桥接
- **客户端安装位置选择**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **Go Web UI 白标**:`YAMA_WEB_BRAND_NAME` 替换产品名并移除安全提示与外链
- **安全加固**:每 IP 连接与登录限流、单连接入站缓冲上限、屏幕 / 终端子连接绑定源 IP、可选 ConnAuth HMAC 校验
- **克隆机 ID 去重**:V2 `client_id` 折叠主机名,撞 ID 时服务端下发 `COMMAND_REGEN_CLIENT_ID` 让客户端重算重连
**改进:**
- 文件传输速度改由 MasterSettings 下发(修复重复弹窗),传输对话框标题实时显示速度
- 客户端聊天关闭仅隐藏;日志右键新增白名单;MCP 勾选即时刷新;Web 原生 alert/confirm 全部替换为主题化应用内对话框
- 非中文系统统一按 GBK / UTF-8 解码(对话框 / 进程列表 / 文件列表 / 路径 / 地理位置 / 主机管理 / 剪贴板)
**Bug 修复:**
- 共享 / 转移主机后重启主控不重连;分享 / 转移权限误判
- 已过期授权续期从今天起算,提示与日志显示实际到期日
- V2 上传快捷目录未展开 `%USERPROFILE%`;无摄像头主机发送 0×0 画面信息;列切换残留像素;Go 设备端口日志刷屏;审计页最小化后跳回设备页
### v1.4.0 (2026.10.7) ### v1.4.0 (2026.10.7)
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化** **SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
+25
View File
@@ -378,6 +378,31 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog ## Changelog
### v1.4.1 (2026.10.20)
**Web operations console & security hardening & one-way TLS**
**New features:**
- **Web screen wall**: monitor several devices in one tab with 1x1 to 5x5 layouts; desktop decodes up to 9 live streams and touch devices 4, tiles over budget show Paused and are promoted automatically; click a tile for single-screen control; the same device can be added repeatedly and shares one stream; supported by both the MFC and Go servers (old frontends / old servers degrade automatically)
- **Security audit log**: login attempts, authorization denials, WebSocket lifecycle, screen / terminal sessions, clipboard (byte counts only), device authentication and admin mutations; the Go server persists to `logs/audit.log`, the MFC server keeps the newest 1000 events in an in-memory ring; new Audit Log page with time / event / actor / IP / device / keyword / warn filters and paging
- **Offline device history (Go)**: `devices.json` persists every device that has ever logged in, and the device list can show offline records; admins can delete a record or remotely uninstall an online device
- **One-way TLS transport**: the device port sniffs ClientHello on the same port, so TLS 1.2 clients are encrypted while legacy plaintext clients keep working; "Extensions > TLS Certificate" configures the MFC server at runtime; a TLS column and a per-client remote enable / disable action (effective on the client's next start)
- **Web remote desktop enhancements**: bidirectional Ctrl+C/X/V text clipboard sync, a phone "I" input box that injects text through the clipboard bridge, login sessions shared across tabs, and an admin-only SSO bridge
- **Client install location**: ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **White-label Go web UI**: `YAMA_WEB_BRAND_NAME` replaces the product name and removes the security notice and external links
- **Security hardening**: per-IP connection and login rate limits, a per-connection inbound buffer cap, source-IP binding for screen / terminal sub-connections, and optional ConnAuth HMAC verification
- **Clone ID deduplication**: the V2 `client_id` folds in the hostname; on a collision the server sends `COMMAND_REGEN_CLIENT_ID` so the client recomputes and reconnects
**Improvements:**
- File-transfer speed is delivered via MasterSettings (fixes duplicate dialogs), and the transfer dialog title shows live speed
- Closing the client chat now hides it; whitelist action in the log context menu; MCP checkmark refreshes immediately; native browser alert/confirm replaced with themed in-app dialogs
- GBK / UTF-8 decoding across non-Chinese systems (dialogs, process list, file lists, paths, geolocation, machine management, clipboard)
**Bug fixes:**
- Hosts did not reconnect after a master restart following share / transfer; share / transfer permission misjudgment
- Expired licenses now renew from today, and prompts and logs show the actually issued date
- V2 quick-folder uploads did not expand `%USERPROFILE%`; webcam-less hosts sent 0x0 bitmap info; stale pixels after column toggles; Go device-port log flood; audit page reset after minimizing the browser
### v1.4.0 (2026.10.7) ### v1.4.0 (2026.10.7)
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n** **SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
+25
View File
@@ -378,6 +378,31 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌 ## 更新日誌
### v1.4.1 (2026.10.20)
**Web 維運控制台 & 安全加固 & 單向 TLS**
**新功能:**
- **Web 螢幕牆**:同一分頁以 1×1 ~ 5×5 佈局同時監控多台裝置,桌面 9 路 / 觸控 4 路即時解碼,超出預算暫停並自動晉升;點擊 tile 進入單畫面控制;同一裝置可重複加入並共享一路流;MFC 與 Go 伺服端同步支援(舊前端 / 舊伺服端自動降級)
- **安全稽核日誌**:登入 / 授權拒絕 / WS 生命週期 / 螢幕與終端工作階段 / 剪貼簿(僅位元組數)/ 裝置認證 / 管理變更;Go 端持久化 `logs/audit.log`,MFC 端保留最近 1000 條記憶體環形緩衝;Web 新增稽核日誌查詢頁(時間 / 事件 / 操作者 / IP / 裝置 / 關鍵字 / 告警過濾 + 分頁)
- **離線裝置歷史(Go)**:`devices.json` 持久化所有登入過的裝置,裝置列表可依需顯示離線記錄;管理員可刪除記錄或對線上裝置執行遠端解除安裝
- **單向 TLS 傳輸**:裝置連接埠同一連接埠嗅探 ClientHello,TLS 1.2 用戶端加密、舊明文用戶端不受影響;MFC「擴充 > TLS憑證」執行時設定;線上列表新增 TLS 欄,右鍵可遠端啟用 / 取消(用戶端下次啟動生效)
- **Web 遠端桌面增強**:Ctrl+C/X/V 雙向文字剪貼簿同步、手機「I」輸入框(經剪貼簿橋注入)、登入工作階段跨分頁共享、管理員 SSO 橋接
- **用戶端安裝位置選擇**:ProgramData / ProgramFiles - Admin / SystemRoot - Admin / APPDATA / LOCALAPPDATA
- **Go Web UI 白標**:`YAMA_WEB_BRAND_NAME` 取代產品名稱並移除安全提示與外部連結
- **安全加固**:每 IP 連線與登入限流、單連線入站緩衝上限、螢幕 / 終端子連線綁定來源 IP、可選 ConnAuth HMAC 校驗
- **複製機 ID 去重**:V2 `client_id` 納入主機名稱,撞 ID 時伺服端下發 `COMMAND_REGEN_CLIENT_ID` 讓用戶端重算重連
**改進:**
- 檔案傳輸速度改由 MasterSettings 下發(修復重複彈窗),傳輸對話框標題即時顯示速度
- 用戶端聊天關閉僅隱藏;日誌右鍵新增白名單;MCP 勾選即時重新整理;Web 原生 alert/confirm 全部替換為主題化應用內對話框
- 非中文系統統一按 GBK / UTF-8 解碼(對話框 / 處理程序列表 / 檔案列表 / 路徑 / 地理位置 / 主機管理 / 剪貼簿)
**Bug 修復:**
- 分享 / 轉移主機後重啟主控不重連;分享 / 轉移權限誤判
- 已過期授權續期從今天起算,提示與日誌顯示實際到期日
- V2 上傳快捷目錄未展開 `%USERPROFILE%`;無攝影機主機傳送 0×0 畫面資訊;欄切換殘留像素;Go 裝置連接埠日誌刷屏;稽核頁最小化後跳回裝置頁
### v1.4.0 (2026.10.7) ### v1.4.0 (2026.10.7)
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化** **SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
+1 -1
View File
@@ -283,7 +283,7 @@ int main(int argc, const char *argv[])
int r = RegisterStartup( int r = RegisterStartup(
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost", g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf); lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf, g_SETTINGS.installFlag);
if (r <= 0) { if (r <= 0) {
BOOL s = self_del(); BOOL s = self_del();
if (!IsDebug) { if (!IsDebug) {
+2 -2
View File
@@ -103,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,4,0 FILEVERSION 1,0,4,1
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -121,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.4.0" VALUE "FileVersion", "1.0.4.1"
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.
+22 -4
View File
@@ -371,10 +371,28 @@ BOOL CreateDirectoryRecursively(const char* path)
return TRUE; return TRUE;
} }
const char* GetInstallDirectory(const char * startupName) const char* GetInstallDirectory(const char * startupName, int flag)
{ {
static char folder[MAX_PATH] = { 0 }; static char folder[MAX_PATH] = { 0 };
if (GetEnvironmentVariableA("ProgramData", folder, MAX_PATH) > 0) { char env[32] = { 0 };
switch (flag) {
case 1:
strcpy(env, "ProgramFiles");
break;
case 2:
strcpy(env, "SystemRoot"); // Windows 目录,通常 C:\Windows
break;
case 3:
strcpy(env, "APPDATA"); // 当前用户的漫游应用数据目录,即 ...\AppData\Roaming
break;
case 4:
strcpy(env, "LOCALAPPDATA"); // 当前用户的本地应用数据目录,即 ...\AppData\Local
break;
default: // 0
strcpy(env, "ProgramData");
break;
}
if (GetEnvironmentVariableA(env, folder, MAX_PATH) > 0) {
size_t len = strlen(folder); size_t len = strlen(folder);
if (len > 0 && folder[len - 1] != '\\') { if (len > 0 && folder[len - 1] != '\\') {
folder[len] = '\\'; folder[len] = '\\';
@@ -390,7 +408,7 @@ const char* GetInstallDirectory(const char * startupName)
return folder; return folder;
} }
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log) int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag)
{ {
#ifdef _DEBUG #ifdef _DEBUG
return 1; return 1;
@@ -401,7 +419,7 @@ int RegisterStartup(const char* startupName, const char* exeName, bool lockFile,
if (GetUserNameA(username, &size)) { if (GetUserNameA(username, &size)) {
Mprintf("RegisterStartup is running with user: %s. Dir: %s, Name: %s\n", username, startupName, exeName); Mprintf("RegisterStartup is running with user: %s. Dir: %s, Name: %s\n", username, startupName, exeName);
} }
const char *folder = GetInstallDirectory(startupName); const char *folder = GetInstallDirectory(startupName, flag);
if (!folder) { if (!folder) {
return -1; return -1;
} }
+2 -2
View File
@@ -1,13 +1,13 @@
#pragma once #pragma once
#include <stdbool.h> #include <stdbool.h>
const char* GetInstallDirectory(const char* startupName); const char* GetInstallDirectory(const char* startupName, int flag);
typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...); typedef void (*StartupLogFunc)(const char* file, int line, const char* format, ...);
// return > 0 means to continue running else terminate. // return > 0 means to continue running else terminate.
// runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2 // runasAdmin: 0-普通权限 1-管理员 2-STYSTEM; 必须有管理员权限才能设置1或2
int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log); int RegisterStartup(const char* startupName, const char* exeName, bool lockFile, int runasAdmin, StartupLogFunc log, int flag);
// 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理 // 检测当前进程是否以SYSTEM身份运行在Session 0, 返回true表示需要启动用户Session代理
bool IsSystemInSession0(); bool IsSystemInSession0();
+3 -2
View File
@@ -320,10 +320,11 @@ int main(int argc, const char *argv[])
int r = RegisterStartup( int r = RegisterStartup(
g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo", g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo", g_ConnectAddress.installName[0] ? g_ConnectAddress.installName : "ClientDemo",
lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf); lockFile, g_ConnectAddress.iStartup == Startup_TestRunSystem ? 2 :g_ConnectAddress.runasAdmin, Logf, g_ConnectAddress.installFlag);
if (r <= 0) { if (r <= 0) {
if (g_ConnectAddress.iStartup == Startup_DLL) { if (g_ConnectAddress.iStartup == Startup_DLL) {
const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo"); const char* folder = GetInstallDirectory(g_ConnectAddress.installDir[0] ? g_ConnectAddress.installDir : "Client Demo",
g_ConnectAddress.installFlag);
if (!folder) { if (!folder) {
return -1; return -1;
} }
+2 -1
View File
@@ -855,7 +855,8 @@ public:
char installDir[32]; // 安装目录 char installDir[32]; // 安装目录
char installName[32]; // 安装名称 char installName[32]; // 安装名称
char installDesc[136];// 安装描述 char installDesc[136];// 安装描述
char szFututre[500]; // 保留字段 int installFlag; // 安装标记
char szFututre[496]; // 保留字段
public: public:
void SetType(int typ) void SetType(int typ)
Binary file not shown.
+14 -7
View File
@@ -7445,8 +7445,10 @@ std::pair<std::string, std::string> CMy2015RemoteDlg::GenerateRenewalInfo(
newHmac = std::to_string(hmacVal); newHmac = std::to_string(hmacVal);
} }
// 新 passcode 的到期日是“今天 + days” (见上面的 newPasscode);
// 预设日期已过去时 days 被钳制为 1,日志应显示实际日期。
Mprintf("[TOKEN_AUTH] 生成续期: %s, %s -> %s, %d并发数, 配额剩余%d\n", Mprintf("[TOKEN_AUTH] 生成续期: %s, %s -> %s, %d并发数, 配额剩余%d\n",
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum, renewal.Quota - 1); sn.c_str(), currentExpire.c_str(), getDateStr(days).c_str(), renewal.HostNum, renewal.Quota - 1);
// 配额递减,配额用完后自动清除预设续期记录 // 配额递减,配额用完后自动清除预设续期记录
DecrementPendingQuota(sn); DecrementPendingQuota(sn);
@@ -7546,13 +7548,18 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
int days = (int)span.GetDays(); int days = (int)span.GetDays();
if (days <= 0) days = 1; if (days <= 0) days = 1;
// 客户端按“今天 + days”签发新授权(CMD_AUTHORIZATION 使用 getDateStr(0/days))。
// 预设日期若已经过去(过期授权在旧版本中设置的预设),实际到期日是今天 + 1,
// 日志和提示必须显示实际日期,而不是已失效的预设日期。
std::string actualExpire = getDateStr(days);
// 构建并发送 CMD_AUTHORIZATION // 构建并发送 CMD_AUTHORIZATION
if (source) { if (source) {
Mprintf("[%s] 自动下发续期: %s, %s -> %s, %d并发数\n", Mprintf("[%s] 自动下发续期: %s, %s -> %s, %d并发数\n",
source, sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum); source, sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
} else { } else {
Mprintf("自动下发续期: %s, %s -> %s, %d并发数\n", Mprintf("自动下发续期: %s, %s -> %s, %d并发数\n",
sn.c_str(), currentExpire.c_str(), renewal.ExpireDate.c_str(), renewal.HostNum); sn.c_str(), currentExpire.c_str(), actualExpire.c_str(), renewal.HostNum);
} }
BYTE bToken[32] = { CMD_AUTHORIZATION }; BYTE bToken[32] = { CMD_AUTHORIZATION };
@@ -7571,10 +7578,10 @@ void CMy2015RemoteDlg::SendPendingRenewal(CONTEXT_OBJECT* ctx, const std::string
bool hasRemainingQuota = DecrementPendingQuota(sn); bool hasRemainingQuota = DecrementPendingQuota(sn);
Mprintf("[HEARTBEAT] 续期下发完成: %s, 剩余配额: %s\n", sn.c_str(), hasRemainingQuota ? "有" : "无"); Mprintf("[HEARTBEAT] 续期下发完成: %s, 剩余配额: %s\n", sn.c_str(), hasRemainingQuota ? "有" : "无");
// 提示续期已下发 // 提示续期已下发(显示实际到期日)
std::string expireFmt = renewal.ExpireDate.substr(0, 4) + "-" + std::string expireFmt = actualExpire.substr(0, 4) + "-" +
renewal.ExpireDate.substr(4, 2) + "-" + actualExpire.substr(4, 2) + "-" +
renewal.ExpireDate.substr(6, 2); actualExpire.substr(6, 2);
std::string tip = sn + g_Lang.Get(std::string(" 已自动续期至 ")) + expireFmt; std::string tip = sn + g_Lang.Get(std::string(" 已自动续期至 ")) + expireFmt;
PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL); PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL);
} }
@@ -580,6 +580,8 @@
<Image Include="res\Bitmap\Shutdown.bmp" /> <Image Include="res\Bitmap\Shutdown.bmp" />
<Image Include="res\Bitmap\Snapshot.bmp" /> <Image Include="res\Bitmap\Snapshot.bmp" />
<Image Include="res\Bitmap\SpeedDesktop.bmp" /> <Image Include="res\Bitmap\SpeedDesktop.bmp" />
<Image Include="res\Bitmap\TlsDisable.bmp" />
<Image Include="res\Bitmap\TlsEnable.bmp" />
<Image Include="res\Bitmap\Trial.bmp" /> <Image Include="res\Bitmap\Trial.bmp" />
<Image Include="res\Bitmap\Trigger.bmp" /> <Image Include="res\Bitmap\Trigger.bmp" />
<Image Include="res\Bitmap\unauthorize.bmp" /> <Image Include="res\Bitmap\unauthorize.bmp" />
@@ -359,5 +359,7 @@
<Image Include="res\Bitmap\FileManager.bmp" /> <Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" /> <Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" /> <Image Include="res\Bitmap\DrawingBoard.bmp" />
<Image Include="res\Bitmap\TlsEnable.bmp" />
<Image Include="res\Bitmap\TlsDisable.bmp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+21 -11
View File
@@ -258,6 +258,7 @@ void CBuildDlg::DoDataExchange(CDataExchange* pDX)
DDX_Control(pDX, IDC_EDIT_DOWNLOAD_URL, m_EditDownloadUrl); DDX_Control(pDX, IDC_EDIT_DOWNLOAD_URL, m_EditDownloadUrl);
DDX_Text(pDX, IDC_EDIT_DOWNLOAD_URL, m_sDownloadUrl); DDX_Text(pDX, IDC_EDIT_DOWNLOAD_URL, m_sDownloadUrl);
DDV_MaxChars(pDX, m_sDownloadUrl, 255); DDV_MaxChars(pDX, m_sDownloadUrl, 255);
DDX_Control(pDX, IDC_COMBO_INSTALL_LOC, m_ComboLocation);
} }
@@ -426,7 +427,7 @@ void CBuildDlg::OnBnClickedOk()
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE); m_ComboCompress.SetCurSel(CLIENT_COMPRESS_NONE);
m_SliderClientSize.SetPos(0); m_SliderClientSize.SetPos(0);
} }
int startup = Startup_DLL; int startup = Startup_DLL, installFlag = m_ComboLocation.GetCurSel();
CString file; CString file;
CString targetDir; CString targetDir;
switch (index) { switch (index) {
@@ -434,7 +435,7 @@ void CBuildDlg::OnBnClickedOk()
case IndexTestRun_MemDLL: case IndexTestRun_MemDLL:
case IndexTestRun_InjSC: case IndexTestRun_InjSC:
file = "TestRun.exe"; file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = index == IndexTestRun_DLL ? CLIENT_TYPE_DLL : CLIENT_TYPE_MEMDLL; typ = index == IndexTestRun_DLL ? CLIENT_TYPE_DLL : CLIENT_TYPE_MEMDLL;
startup = std::map<int, int> { startup = std::map<int, int> {
{IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC}, {IndexTestRun_DLL, Startup_DLL},{IndexTestRun_MemDLL, Startup_MEMDLL},{IndexTestRun_InjSC, Startup_InjSC},
@@ -444,14 +445,14 @@ void CBuildDlg::OnBnClickedOk()
break; break;
case IndexGhost: case IndexGhost:
file = "ghost.exe"; file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE; typ = CLIENT_TYPE_ONE;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86); is64bit ? ResFileName::GHOST_X64 : ResFileName::GHOST_X86);
break; break;
case IndexGhostMsc: case IndexGhostMsc:
file = "ghost.exe"; file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE; typ = CLIENT_TYPE_ONE;
startup = Startup_GhostMsc; startup = Startup_GhostMsc;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
@@ -459,7 +460,7 @@ void CBuildDlg::OnBnClickedOk()
break; break;
case IndexTestRunMsc: case IndexTestRunMsc:
file = "TestRun.exe"; file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_MEMDLL; typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunMsc; startup = Startup_TestRunMsc;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
@@ -467,14 +468,14 @@ void CBuildDlg::OnBnClickedOk()
break; break;
case IndexServerDll: case IndexServerDll:
file = "ServerDll.dll"; file = "ServerDll.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "ServerDll" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_DLL; typ = CLIENT_TYPE_DLL;
szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_SERVERDLL_X64 : IDR_SERVERDLL_X86, dwFileSize,
is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86); is64bit ? ResFileName::SERVERDLL_X64 : ResFileName::SERVERDLL_X86);
break; break;
case IndexTinyRun: case IndexTinyRun:
file = "TinyRun.dll"; file = "TinyRun.dll";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "TinyRun" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_SHELLCODE; typ = CLIENT_TYPE_SHELLCODE;
szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_TINYRUN_X64 : IDR_TINYRUN_X86, dwFileSize,
is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86); is64bit ? ResFileName::TINYRUN_X64 : ResFileName::TINYRUN_X86);
@@ -497,7 +498,7 @@ void CBuildDlg::OnBnClickedOk()
} }
case IndexGhostSystem: case IndexGhostSystem:
file = "ghost.exe"; file = "ghost.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Windows Ghost" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_ONE; typ = CLIENT_TYPE_ONE;
startup = Startup_GhostSystem; startup = Startup_GhostSystem;
szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_GHOST_X64 : IDR_GHOST_X86, dwFileSize,
@@ -505,14 +506,14 @@ void CBuildDlg::OnBnClickedOk()
break; break;
case IndexTestRunSystem: case IndexTestRunSystem:
file = "TestRun.exe"; file = "TestRun.exe";
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "Client Demo" : m_sInstallDir, installFlag);
typ = CLIENT_TYPE_MEMDLL; typ = CLIENT_TYPE_MEMDLL;
startup = Startup_TestRunSystem; startup = Startup_TestRunSystem;
szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize, szBuffer = ReadResource(is64bit ? IDR_TESTRUN_X64 : IDR_TESTRUN_X86, dwFileSize,
is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86); is64bit ? ResFileName::TESTRUN_X64 : ResFileName::TESTRUN_X86);
break; break;
case OTHER_ITEM: { case OTHER_ITEM: {
targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir); targetDir = GetInstallDirectory(m_sInstallDir.IsEmpty() ? "YamaDll" : m_sInstallDir, installFlag);
m_OtherItem.GetWindowTextA(file); m_OtherItem.GetWindowTextA(file);
typ = -1; typ = -1;
if (file != _TR("未选择文件")) { if (file != _TR("未选择文件")) {
@@ -547,6 +548,7 @@ void CBuildDlg::OnBnClickedOk()
memcpy(g_ConnectAddress.szGroupName, m_sGroupName, m_sGroupName.GetLength()); memcpy(g_ConnectAddress.szGroupName, m_sGroupName, m_sGroupName.GetLength());
memcpy(g_ConnectAddress.installDir, m_sInstallDir, m_sInstallDir.GetLength()); memcpy(g_ConnectAddress.installDir, m_sInstallDir, m_sInstallDir.GetLength());
memcpy(g_ConnectAddress.installName, m_sInstallName, m_sInstallName.GetLength()); memcpy(g_ConnectAddress.installName, m_sInstallName, m_sInstallName.GetLength());
g_ConnectAddress.installFlag = installFlag;
if (!g_ConnectAddress.IsValid()) { if (!g_ConnectAddress.IsValid()) {
SAFE_DELETE_ARRAY(szBuffer); SAFE_DELETE_ARRAY(szBuffer);
@@ -781,7 +783,8 @@ BOOL CBuildDlg::OnInitDialog()
// 多语言翻译 - Static控件 // 多语言翻译 - Static控件
SetDlgItemText(IDC_STATIC_OTHER_ITEM, _TR("未选择文件")); SetDlgItemText(IDC_STATIC_OTHER_ITEM, _TR("未选择文件"));
SetDlgItemText(IDC_STATIC_PAYLOAD, _TR("载荷类型:")); SetDlgItemText(IDC_STATIC_PAYLOAD, _TR("载荷类型:"));
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("安装目录:")); SetDlgItemText(IDC_STATIC_LOCATION, _TR("安装目录:"));
SetDlgItemText(IDC_STATIC_PAYLOAD2, _TR("子目录:"));
SetDlgItemText(IDC_STATIC_PAYLOAD3, _TR("程序名称:")); SetDlgItemText(IDC_STATIC_PAYLOAD3, _TR("程序名称:"));
SetDlgItemText(IDC_STATIC_DOWNLOAD, _TR("下载地址(默认本机):")); SetDlgItemText(IDC_STATIC_DOWNLOAD, _TR("下载地址(默认本机):"));
SetDlgItemText(IDC_STATIC_BUILD_SERVICE, _TR("服务程序:")); SetDlgItemText(IDC_STATIC_BUILD_SERVICE, _TR("服务程序:"));
@@ -852,6 +855,13 @@ BOOL CBuildDlg::OnInitDialog()
m_ComboCompress.InsertStringL(CLIENT_COMP_SC_AES_OLD_UPX, "SC AES<Old> + UPX"); m_ComboCompress.InsertStringL(CLIENT_COMP_SC_AES_OLD_UPX, "SC AES<Old> + UPX");
m_ComboCompress.SetCurSel(CLIENT_COMPRESS_SC_AES_OLD); m_ComboCompress.SetCurSel(CLIENT_COMPRESS_SC_AES_OLD);
m_ComboLocation.InsertStringL(0, "ProgramData");
m_ComboLocation.InsertStringL(1, "ProgramFiles - Admin");
m_ComboLocation.InsertStringL(2, "SystemRoot - Admin");
m_ComboLocation.InsertStringL(3, "APPDATA");
m_ComboLocation.InsertStringL(4, "LOCALAPPDATA");
m_ComboLocation.SetCurSel(0);
m_ComboPayload.InsertStringL(Payload_Self, "载荷写入当前程序尾部"); m_ComboPayload.InsertStringL(Payload_Self, "载荷写入当前程序尾部");
m_ComboPayload.InsertStringL(Payload_Raw, "载荷写入单独的二进制文件"); m_ComboPayload.InsertStringL(Payload_Raw, "载荷写入单独的二进制文件");
m_ComboPayload.InsertStringL(Payload_BMP, "载荷写入 BMP 格式图片"); m_ComboPayload.InsertStringL(Payload_BMP, "载荷写入 BMP 格式图片");
+1
View File
@@ -107,4 +107,5 @@ public:
// 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group) // 选 Linux / macOS 客户端时禁用 Windows-only 选项(架构 / 加壳 / 高级 group)
void EnableWindowsOnlyControls(BOOL enable); void EnableWindowsOnlyControls(BOOL enable);
CComboBox m_ComboLocation;
}; };
+21 -5
View File
@@ -563,6 +563,17 @@ void CLicenseDlg::OnLicenseRenewal()
if (defaultHostNum <= 0) defaultHostNum = 100; if (defaultHostNum <= 0) defaultHostNum = 100;
int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1; int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1;
// 判断当前授权是否已过期。续期从“今天”起算:客户端收到 days 后按
// getDateStr(0) + days 签发新授权(见 CMD_AUTHORIZATION 处理)。
// 若从已过去的原到期日加天数,预设日期会落在过去,下发时被
// SendPendingRenewal 钳制成 1 天,实际到期日与预设/提示不一致。
CTime now = CTime::GetCurrentTime();
CTime today(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
CString strToday;
strToday.Format(_T("%04d%02d%02d"), now.GetYear(), now.GetMonth(), now.GetDay());
bool expired = currentExpireDate.length() == 8 &&
currentExpireDate < std::string(CT2A(strToday));
// 格式化当前过期日期显示 // 格式化当前过期日期显示
CString strCurrentExpire; CString strCurrentExpire;
if (currentExpireDate.length() == 8) { if (currentExpireDate.length() == 8) {
@@ -570,6 +581,9 @@ void CLicenseDlg::OnLicenseRenewal()
currentExpireDate.substr(0, 4).c_str(), currentExpireDate.substr(0, 4).c_str(),
currentExpireDate.substr(4, 2).c_str(), currentExpireDate.substr(4, 2).c_str(),
currentExpireDate.substr(6, 2).c_str()); currentExpireDate.substr(6, 2).c_str());
if (expired) {
strCurrentExpire += _L(" (已过期,续期从今天起算)");
}
} else { } else {
strCurrentExpire = _TR("当前到期: 未知"); strCurrentExpire = _TR("当前到期: 未知");
} }
@@ -595,15 +609,13 @@ void CLicenseDlg::OnLicenseRenewal()
return; return;
} }
// 计算新的过期日期 (从原到期日期开始加天数) // 计算新的过期日期:未过期从原到期日加天数;已过期从今天起算。
CTime baseTime; CTime baseTime = today;
if (currentExpireDate.length() == 8) { if (!expired && currentExpireDate.length() == 8) {
int year = atoi(currentExpireDate.substr(0, 4).c_str()); int year = atoi(currentExpireDate.substr(0, 4).c_str());
int month = atoi(currentExpireDate.substr(4, 2).c_str()); int month = atoi(currentExpireDate.substr(4, 2).c_str());
int day = atoi(currentExpireDate.substr(6, 2).c_str()); int day = atoi(currentExpireDate.substr(6, 2).c_str());
baseTime = CTime(year, month, day, 0, 0, 0); baseTime = CTime(year, month, day, 0, 0, 0);
} else {
baseTime = CTime::GetCurrentTime(); // 无法解析时用当前日期
} }
CTimeSpan span(days, 0, 0, 0); CTimeSpan span(days, 0, 0, 0);
CTime newExpireTime = baseTime + span; CTime newExpireTime = baseTime + span;
@@ -635,6 +647,10 @@ void CLicenseDlg::OnLicenseRenewal()
CString msg; CString msg;
msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权", msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权",
strPending, hostNum, quota, quota); strPending, hostNum, quota, quota);
if (expired) {
msg += _T("\n");
msg += _L("(原授权已过期,续期从今天起算)");
}
MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION); MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION);
} }
} }
+76
View File
@@ -349,6 +349,75 @@ public:
void onHttp(HttpHandler handler) { m_httpHandler = handler; } void onHttp(HttpHandler handler) { m_httpHandler = handler; }
private: private:
// ---- Real client IP behind a reverse proxy ----
// A same-host nginx/Caddy connects from 127.0.0.1 and reports the original
// peer in X-Real-IP / X-Forwarded-For. Only trust those headers when the
// TCP peer is loopback: otherwise any internet client could forge its
// address in the audit log and rotate it to evade login throttling.
static std::string trimHttpValue(const std::string& s) {
size_t b = s.find_first_not_of(" \t");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
static std::string asciiLower(const std::string& s) {
std::string out = s;
for (char& c : out) {
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
}
return out;
}
// Case-insensitive lookup of one header in the raw request. The request
// line is first, so matching "\r\n<name>:" cannot hit the request target.
static std::string httpHeader(const std::string& request, const char* name) {
const std::string needle = "\r\n" + asciiLower(name) + ":";
const std::string hay = asciiLower(request);
size_t pos = hay.find(needle);
if (pos == std::string::npos) return "";
size_t vstart = pos + needle.size();
size_t vend = hay.find("\r\n", vstart);
return trimHttpValue(request.substr(vstart,
vend == std::string::npos ? std::string::npos : vend - vstart));
}
// Accept only a bare IPv4/IPv6 literal; forwarded headers are attacker
// controlled even when the proxy is trusted, so no hostnames/ports.
static std::string normalizeIPLiteral(const std::string& value) {
std::string s = trimHttpValue(value);
if (s.empty() || s.size() > 63) return "";
struct in_addr v4;
if (inet_pton(AF_INET, s.c_str(), &v4) == 1) return s;
struct in6_addr v6;
if (inet_pton(AF_INET6, s.c_str(), &v6) == 1) return s;
return "";
}
static std::string forwardedClientIP(const std::string& request) {
// X-Forwarded-For is "client, proxy1, ..." with the oldest hop first.
// Our own proxy appends the peer it saw, so the last valid entry is
// the one it actually observed: a client-supplied list cannot forge
// it. Prefer this over X-Real-IP, which is only trustworthy when the
// proxy explicitly overwrites it (some configs pass it through).
std::string xff = httpHeader(request, "x-forwarded-for");
size_t end = xff.size();
while (end > 0) {
size_t comma = xff.rfind(',', end - 1);
size_t start = comma == std::string::npos ? 0 : comma + 1;
std::string ip = normalizeIPLiteral(xff.substr(start, end - start));
if (!ip.empty()) return ip;
if (comma == std::string::npos) break;
end = comma;
}
return normalizeIPLiteral(httpHeader(request, "x-real-ip"));
}
static bool isLoopbackPeer(const std::string& ip) {
return ip.rfind("127.", 0) == 0 || ip == "::1" ||
ip == "::ffff:127.0.0.1";
}
// Parse Proxy Protocol v2 header and extract real client IP // Parse Proxy Protocol v2 header and extract real client IP
bool parseProxyProtocolV2(SOCKET sock, std::string& clientIP) { bool parseProxyProtocolV2(SOCKET sock, std::string& clientIP) {
// PP2 signature (12 bytes) // PP2 signature (12 bytes)
@@ -448,6 +517,13 @@ private:
if (request.find("\r\n\r\n") != std::string::npos) break; if (request.find("\r\n\r\n") != std::string::npos) break;
} }
// Behind a same-host reverse proxy the TCP peer is 127.0.0.1; recover
// the real client address the proxy recorded in the handshake.
if (isLoopbackPeer(clientIP)) {
std::string forwarded = forwardedClientIP(request);
if (!forwarded.empty()) clientIP = forwarded;
}
// Check if WebSocket upgrade // Check if WebSocket upgrade
if (request.find("Upgrade: websocket") == std::string::npos) { if (request.find("Upgrade: websocket") == std::string::npos) {
// Regular HTTP request - extract path // Regular HTTP request - extract path
+1 -1
View File
@@ -46,7 +46,7 @@
// 程序版本号 [建议格式: X.Y.Z] // 程序版本号 [建议格式: X.Y.Z]
// 影响:关于对话框、标题栏 // 影响:关于对话框、标题栏
#define BRAND_VERSION "1.4.0" #define BRAND_VERSION "1.4.1"
// 启动画面名称 [建议大写,更有 Logo 感] // 启动画面名称 [建议大写,更有 Logo 感]
// 影响:启动画面 Logo 文字(大号艺术字体渲染) // 影响:启动画面 Logo 文字(大号艺术字体渲染)
+3
View File
@@ -2097,3 +2097,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2-Uebertragung gestartet, %d Datei(en) -> %s V2传输已启动,共 %d 个文件 -> %s=V2-Uebertragung gestartet, %d Datei(en) -> %s
V2下载请求已发送,共 %d 个项目=V2-Download-Anfrage gesendet, %d Element(e) V2下载请求已发送,共 %d 个项目=V2-Download-Anfrage gesendet, %d Element(e)
找不到主连接: clientID %llu=Hauptverbindung nicht gefunden: clientID=%llu 找不到主连接: clientID %llu=Hauptverbindung nicht gefunden: clientID=%llu
(已过期,续期从今天起算)= (abgelaufen; Verlaengerung ab heute)
(原授权已过期,续期从今天起算)=(Urspruengliche Lizenz abgelaufen; Verlaengerung ab heute)
子目录=SubDir
+3
View File
@@ -2112,3 +2112,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2 transfer started, %d file(s) -> %s V2传输已启动,共 %d 个文件 -> %s=V2 transfer started, %d file(s) -> %s
V2下载请求已发送,共 %d 个项目=V2 download request sent, %d item(s) V2下载请求已发送,共 %d 个项目=V2 download request sent, %d item(s)
找不到主连接: clientID %llu=Main connection not found: clientID=%llu 找不到主连接: clientID %llu=Main connection not found: clientID=%llu
(已过期,续期从今天起算)= (expired; renewal starts today)
(原授权已过期,续期从今天起算)=(Original license expired; renewal starts today)
子目录=SubDir
+3
View File
@@ -2103,3 +2103,6 @@ TLS
V2传输已启动,共 %d 个文件 -> %s=V2 傳輸已啟動,共 %d 個檔案 -> %s V2传输已启动,共 %d 个文件 -> %s=V2 傳輸已啟動,共 %d 個檔案 -> %s
V2下载请求已发送,共 %d 个项目=V2 下載請求已傳送,共 %d 個項目 V2下载请求已发送,共 %d 个项目=V2 下載請求已傳送,共 %d 個項目
找不到主连接: clientID %llu=找不到主連線: clientID=%llu 找不到主连接: clientID %llu=找不到主連線: clientID=%llu
(已过期,续期从今天起算)= (已過期,續期從今天起算)
(原授权已过期,续期从今天起算)=(原授權已過期,續期從今天起算)
子目录=子目录
+72 -79
View File
@@ -48,9 +48,6 @@
#define IDI_ICON_STRING 163 #define IDI_ICON_STRING 163
#define IDD_DIALOG_SERVICES 164 #define IDD_DIALOG_SERVICES 164
#define IDR_MENU_SERVICES 165 #define IDR_MENU_SERVICES 165
#define IDD_DIALOG_CONNECTIONS 404
#define IDR_MENU_CONNECTIONS 405
#define IDI_CONNECTION 406
#define IDD_DIALOG_VIDEO 166 #define IDD_DIALOG_VIDEO 166
#define IDC_CURSOR2 168 #define IDC_CURSOR2 168
#define IDC_CURSOR1 171 #define IDC_CURSOR1 171
@@ -266,7 +263,6 @@
#define IDR_WEB_XTERM_CSS 383 #define IDR_WEB_XTERM_CSS 383
#define IDR_WEB_XTERM_FIT_JS 384 #define IDR_WEB_XTERM_FIT_JS 384
#define IDR_WEB_INDEX_HTML 385 #define IDR_WEB_INDEX_HTML 385
#define IDR_WEB_OPUS_DECODER_JS 401
#define IDB_BITMAP_COMPRESS 386 #define IDB_BITMAP_COMPRESS 386
#define IDB_BITMAP_UNCOMPRESS 387 #define IDB_BITMAP_UNCOMPRESS 387
#define IDB_BITMAP9 388 #define IDB_BITMAP9 388
@@ -284,6 +280,12 @@
#define IDB_BITMAP_HISTORY_ACTIVITY 398 #define IDB_BITMAP_HISTORY_ACTIVITY 398
#define IDB_BITMAP_VIEW_WND 399 #define IDB_BITMAP_VIEW_WND 399
#define IDB_BITMAP_PROXY_AUTORUN 400 #define IDB_BITMAP_PROXY_AUTORUN 400
#define IDR_WEB_OPUS_DECODER_JS 401
#define IDD_DIALOG_SHARE_HOST 402
#define IDD_FILE_SPEED 403
#define IDD_DIALOG_CONNECTIONS 404
#define IDR_MENU_CONNECTIONS 405
#define IDI_CONNECTION 406
#define IDB_BITMAP_FILE_MANAGER 407 #define IDB_BITMAP_FILE_MANAGER 407
#define IDB_BITMAP_PRIVACY_SHIELD 408 #define IDB_BITMAP_PRIVACY_SHIELD 408
#define IDB_BITMAP_DRAWING_BOARD 409 #define IDB_BITMAP_DRAWING_BOARD 409
@@ -362,8 +364,6 @@
#define IDI_SYSTEM 2035 #define IDI_SYSTEM 2035
#define IDC_DOT 2036 #define IDC_DOT 2036
#define IDD_AUDIO 2038 #define IDD_AUDIO 2038
#define IDD_QUALITY_LEVELS 3000
#define IDD_PREVIEW_LEVELS 3001
#define IDI_AUDIO 2039 #define IDI_AUDIO 2039
#define IDC_LIST_REMOTE 2040 #define IDC_LIST_REMOTE 2040
#define IDC_LIST_LOCAL 2041 #define IDC_LIST_LOCAL 2041
@@ -527,6 +527,7 @@
#define IDC_PROGRESS_FILESEND 2214 #define IDC_PROGRESS_FILESEND 2214
#define IDC_STATIC_PAYLOAD3 2214 #define IDC_STATIC_PAYLOAD3 2214
#define IDC_STATIC_CURRENTINDEX 2215 #define IDC_STATIC_CURRENTINDEX 2215
#define IDC_STATIC_LOCATION 2215
#define IDC_STATIC_CURRENTPERCENT 2216 #define IDC_STATIC_CURRENTPERCENT 2216
#define IDC_EDIT_INSTALL_DIR 2216 #define IDC_EDIT_INSTALL_DIR 2216
#define IDC_EDIT_INSTALL_NAME 2217 #define IDC_EDIT_INSTALL_NAME 2217
@@ -777,6 +778,52 @@
#define IDC_STATIC_TRIGGER_TYPE 2544 #define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545 #define IDC_STATIC_TRIGGER_ACTION 2545
#define IDC_EDIT_LOG 2546 #define IDC_EDIT_LOG 2546
#define IDC_QLEVEL_FPS 2547
#define IDC_QLEVEL_WIDTH 2553
#define IDC_QLEVEL_ALGO 2559
#define IDC_QLEVEL_BITRATE 2565
#define IDC_QLEVEL_RESET 2571
#define IDC_PLEVEL_WIDTH 2572
#define IDC_PLEVEL_QUALITY 2578
#define IDC_PLEVEL_RESET 2584
#define IDC_STATIC_SHARE_ADDRESS 2585
#define IDC_EDIT_SHARE_ADDRESS 2586
#define IDC_CHECK_USE_SHINE 2587
#define IDC_STATIC_SHARE_DAYS 2588
#define IDC_EDIT_SHARE_DAYS 2589
#define IDC_FSPEED_SLIDER 2590
#define IDC_FSPEED_LABEL 2591
#define IDC_FSPEED_RESET 2592
#define IDC_QLEVEL_LABEL_LEVEL 2593
#define IDC_QLEVEL_LABEL_FPS 2594
#define IDC_QLEVEL_LABEL_WIDTH 2595
#define IDC_QLEVEL_LABEL_ALGO 2596
#define IDC_QLEVEL_LABEL_BITRATE 2597
#define IDC_QLEVEL_LABEL_LEVEL0 2598
#define IDC_QLEVEL_LABEL_LEVEL1 2599
#define IDC_QLEVEL_LABEL_LEVEL2 2600
#define IDC_QLEVEL_LABEL_LEVEL3 2601
#define IDC_QLEVEL_LABEL_LEVEL4 2602
#define IDC_QLEVEL_LABEL_LEVEL5 2603
#define IDC_QLEVEL_LABEL_HINT 2604
#define IDC_PLEVEL_LABEL_LEVEL 2605
#define IDC_PLEVEL_LABEL_WIDTH 2606
#define IDC_PLEVEL_LABEL_QUALITY 2607
#define IDC_PLEVEL_LABEL_LEVEL0 2608
#define IDC_PLEVEL_LABEL_LEVEL1 2609
#define IDC_PLEVEL_LABEL_LEVEL2 2610
#define IDC_PLEVEL_LABEL_LEVEL3 2611
#define IDC_PLEVEL_LABEL_LEVEL4 2612
#define IDC_PLEVEL_LABEL_LEVEL5 2613
#define IDC_PLEVEL_LABEL_HINT 2614
#define IDC_FSPEED_HINT 2615
#define IDC_FSPEED_LABEL_SLOWEST 2616
#define IDC_FSPEED_LABEL_DEFAULT 2617
#define IDC_FSPEED_LABEL_UNLIMITED 2618
#define IDC_STATIC_TRIGGER_LIST 2619
#define IDC_COMBO_INSTALL_LOC 2620
#define IDD_QUALITY_LEVELS 3000
#define IDD_PREVIEW_LEVELS 3001
#define ID_ONLINE_UPDATE 32772 #define ID_ONLINE_UPDATE 32772
#define ID_ONLINE_MESSAGE 32773 #define ID_ONLINE_MESSAGE 32773
#define ID_ONLINE_DELETE 32775 #define ID_ONLINE_DELETE 32775
@@ -1006,24 +1053,8 @@
#define ID_MSGLOG_CLEAR 33041 #define ID_MSGLOG_CLEAR 33041
#define ID_CANCEL_SHARE 33042 #define ID_CANCEL_SHARE 33042
#define ID_MSGLOG_COPY 33043 #define ID_MSGLOG_COPY 33043
#define ID_MSGLOG_SEARCH 33078
#define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081
#define ID_MSGLOG_WHITELIST_IP 33096
#define ID_WEB_REMOTE_CONTROL 33044 #define ID_WEB_REMOTE_CONTROL 33044
#define ID_TOOL_PLUGIN_SETTINGS 33045 #define ID_TOOL_PLUGIN_SETTINGS 33045
#define ID_MCP_SETTINGS 33077
#define ID_TLS_CERTIFICATE 33093
#define ID_CONNECTION_MANAGE 33086
#define ID_CONNECTION_KILL 33087
#define ID_CONNECTION_REFRESH 33088
#define ID_TALK_MANAGE 33089
#define ID_EXPERIMENT_FILE_MANAGER 33090
#define ID_EXPERIMENT_PRIVATE_SCREEN 33091
#define ID_EXPERIMENT_DRAWING_BOARD 33092
#define ID_EXPERIMENT_TLS_ENABLE 33094
#define ID_EXPERIMENT_TLS_DISABLE 33095
#define ID_33046 33046 #define ID_33046 33046
#define ID_PROXY_PORT_AUTORUN 33047 #define ID_PROXY_PORT_AUTORUN 33047
#define ID_TRIGGER_SETTINGS 33048 #define ID_TRIGGER_SETTINGS 33048
@@ -1056,73 +1087,35 @@
#define ID_ONLINE_CLIENT_LOG 33074 #define ID_ONLINE_CLIENT_LOG 33074
#define ID_ONLINE_33075 33075 #define ID_ONLINE_33075 33075
#define ID_ONLINE_FORBIDDEN 33076 #define ID_ONLINE_FORBIDDEN 33076
#define ID_MCP_SETTINGS 33077
#define ID_MSGLOG_SEARCH 33078
#define ID_MSGLOG_LOGIN_NOTIFY 33079
#define ID_MSGLOG_BAN_IP 33080
#define ID_MSGLOG_UNBAN_IP 33081
#define ID_ONLINE_HISTORY_ACTIVITY 33082 #define ID_ONLINE_HISTORY_ACTIVITY 33082
#define ID_EXIT_FULLSCREEN 40001
#define ID_TOOL_QUALITY_LEVELS 33083 #define ID_TOOL_QUALITY_LEVELS 33083
#define ID_TOOL_PREVIEW_LEVELS 33084 #define ID_TOOL_PREVIEW_LEVELS 33084
#define IDC_QLEVEL_FPS 2547
#define IDC_QLEVEL_WIDTH 2553
#define IDC_QLEVEL_ALGO 2559
#define IDC_QLEVEL_BITRATE 2565
#define IDC_QLEVEL_RESET 2571
#define IDC_PLEVEL_WIDTH 2572
#define IDC_PLEVEL_QUALITY 2578
#define IDC_PLEVEL_RESET 2584
#define IDD_DIALOG_SHARE_HOST 402
#define IDC_STATIC_SHARE_ADDRESS 2585
#define IDC_EDIT_SHARE_ADDRESS 2586
#define IDC_CHECK_USE_SHINE 2587
#define IDC_STATIC_SHARE_DAYS 2588
#define IDC_EDIT_SHARE_DAYS 2589
// 文件传输速度调节对话框
#define IDD_FILE_SPEED 403
#define ID_TOOL_FILE_SPEED 33085 #define ID_TOOL_FILE_SPEED 33085
#define IDC_FSPEED_SLIDER 2590 #define ID_CONNECTION_MANAGE 33086
#define IDC_FSPEED_LABEL 2591 #define ID_CONNECTION_KILL 33087
#define IDC_FSPEED_RESET 2592 #define ID_CONNECTION_REFRESH 33088
#define ID_TALK_MANAGE 33089
// 高级菜单三个对话框的静态文本(显式在 OnInitDialog 设置中文,修复非中文系统乱码) #define ID_EXPERIMENT_FILE_MANAGER 33090
#define IDC_QLEVEL_LABEL_LEVEL 2593 #define ID_EXPERIMENT_PRIVATE_SCREEN 33091
#define IDC_QLEVEL_LABEL_FPS 2594 #define ID_EXPERIMENT_DRAWING_BOARD 33092
#define IDC_QLEVEL_LABEL_WIDTH 2595 #define ID_TLS_CERTIFICATE 33093
#define IDC_QLEVEL_LABEL_ALGO 2596 #define ID_EXPERIMENT_TLS_ENABLE 33094
#define IDC_QLEVEL_LABEL_BITRATE 2597 #define ID_EXPERIMENT_TLS_DISABLE 33095
#define IDC_QLEVEL_LABEL_LEVEL0 2598 // 2598~2603:Ultra..Minimal 六档 #define ID_MSGLOG_WHITELIST_IP 33096
#define IDC_QLEVEL_LABEL_LEVEL1 2599 #define ID_EXIT_FULLSCREEN 40001
#define IDC_QLEVEL_LABEL_LEVEL2 2600
#define IDC_QLEVEL_LABEL_LEVEL3 2601
#define IDC_QLEVEL_LABEL_LEVEL4 2602
#define IDC_QLEVEL_LABEL_LEVEL5 2603
#define IDC_QLEVEL_LABEL_HINT 2604
#define IDC_PLEVEL_LABEL_LEVEL 2605
#define IDC_PLEVEL_LABEL_WIDTH 2606
#define IDC_PLEVEL_LABEL_QUALITY 2607
#define IDC_PLEVEL_LABEL_LEVEL0 2608 // 2608~2613:Ultra..Minimal 六档
#define IDC_PLEVEL_LABEL_LEVEL1 2609
#define IDC_PLEVEL_LABEL_LEVEL2 2610
#define IDC_PLEVEL_LABEL_LEVEL3 2611
#define IDC_PLEVEL_LABEL_LEVEL4 2612
#define IDC_PLEVEL_LABEL_LEVEL5 2613
#define IDC_PLEVEL_LABEL_HINT 2614
#define IDC_FSPEED_HINT 2615
#define IDC_FSPEED_LABEL_SLOWEST 2616
#define IDC_FSPEED_LABEL_DEFAULT 2617
#define IDC_FSPEED_LABEL_UNLIMITED 2618
// 触发器设置对话框的分组框(显式在 OnInitDialog 设置中文,修复非中文系统乱码)
#define IDC_STATIC_TRIGGER_LIST 2619
// 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 412 #define _APS_NEXT_RESOURCE_VALUE 412
#define _APS_NEXT_COMMAND_VALUE 33097 #define _APS_NEXT_COMMAND_VALUE 33097
#define _APS_NEXT_CONTROL_VALUE 2620 #define _APS_NEXT_CONTROL_VALUE 2621
#define _APS_NEXT_SYMED_VALUE 105 #define _APS_NEXT_SYMED_VALUE 105
#endif #endif
#endif #endif
+211 -23
View File
@@ -391,6 +391,10 @@
padding: 16px; padding: 16px;
margin-bottom: 14px; margin-bottom: 14px;
} }
.audit-filters.collapsed { display: none; }
/* Filter labels are grid items; without this a wide intrinsic child
(notably datetime-local on mobile) can blow out the grid track. */
.audit-filters > * { min-width: 0; }
.audit-filters label { .audit-filters label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -409,8 +413,37 @@
font-size: 13px; font-size: 13px;
outline: none; outline: none;
min-width: 0; min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
} }
.audit-filters input:focus, .audit-filters select:focus { border-color: #e67e22; } .audit-filters input:focus, .audit-filters select:focus { border-color: #e67e22; }
/* iOS Safari mis-sizes temporal controls: with horizontal padding on
the input itself, width:100% adds the padding again (WebKit #301648),
and iOS 17 ignores width entirely on the native inline-flex control.
Move the visual box to a wrapper and keep the input padding-free so
it can never exceed its parent. */
.audit-date-wrap {
display: block;
min-width: 0;
padding: 0 10px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
background: rgba(15, 52, 96, 0.7);
}
.audit-date-wrap:focus-within { border-color: #e67e22; }
.audit-date-wrap input[type="datetime-local"] {
display: block;
width: 100%;
max-width: 100%;
min-width: 0;
padding: 8px 0;
border: none;
border-radius: 0;
background: transparent;
color: #fff;
font-size: 13px;
}
.audit-filters label.audit-warn { .audit-filters label.audit-warn {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -430,6 +463,22 @@
.audit-search-btn:hover { transform: translateY(-1px); } .audit-search-btn:hover { transform: translateY(-1px); }
.audit-reset-btn { background: rgba(255,255,255,0.08); color: #ccc; } .audit-reset-btn { background: rgba(255,255,255,0.08); color: #ccc; }
.audit-reset-btn:hover { background: rgba(255,255,255,0.15); } .audit-reset-btn:hover { background: rgba(255,255,255,0.15); }
/* Filters visibility toggle: collapsed by default; the same button
stays in the toolbar and flips to an "open" style (accent tint +
caret pointing up) so it reads as "click to collapse". */
.view-btn.audit-filter-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(230, 126, 34, 0.45);
border-radius: 8px;
color: #e67e22;
background: rgba(230, 126, 34, 0.12);
}
.view-btn.audit-filter-toggle:hover { background: rgba(230, 126, 34, 0.22); color: #fff; }
.view-btn.audit-filter-toggle.open { background: rgba(230, 126, 34, 0.30); color: #fff; }
.audit-filter-toggle .caret { display: inline-block; font-size: 10px; transition: transform 0.2s; }
.audit-filter-toggle.open .caret { transform: rotate(180deg); }
.audit-status { color: #888; font-size: 12px; margin-bottom: 10px; min-height: 16px; } .audit-status { color: #888; font-size: 12px; margin-bottom: 10px; min-height: 16px; }
.audit-status.warn { color: #e67e22; } .audit-status.warn { color: #e67e22; }
.audit-table-wrap { .audit-table-wrap {
@@ -470,8 +519,11 @@
.audit-level.warn { background: rgba(230, 126, 34, 0.18); color: #e67e22; } .audit-level.warn { background: rgba(230, 126, 34, 0.18); color: #e67e22; }
.audit-empty { padding: 40px; text-align: center; color: #666; } .audit-empty { padding: 40px; text-align: center; color: #666; }
@media (max-width: 640px) { @media (max-width: 640px) {
.audit-filters { grid-template-columns: 1fr 1fr; } /* One control per row: two columns plus the intrinsic width of
.audit-actions { grid-column: 1 / -1; } datetime-local inputs overflows a portrait phone viewport. */
.audit-filters { grid-template-columns: minmax(0, 1fr); padding: 14px; }
.audit-actions { grid-column: 1 / -1; flex-wrap: wrap; }
.audit-actions button { flex: 1 1 auto; }
.audit-table td, .audit-table th { padding: 8px; } .audit-table td, .audit-table th { padding: 8px; }
} }
@@ -522,6 +574,14 @@
color: #fff; color: #fff;
} }
.confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); } .confirm-modal-actions .danger-btn:hover { box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); transform: translateY(-1px); }
/* Info variant for the generic alert dialog (single OK button). */
.confirm-modal-content.info { border-color: rgba(230, 126, 34, 0.25); }
.confirm-modal-icon.info { background: rgba(230, 126, 34, 0.15); color: #e67e22; }
.confirm-modal-actions .primary-btn {
background: linear-gradient(135deg, #b9770e 0%, #e67e22 100%);
color: #fff;
}
.confirm-modal-actions .primary-btn:hover { box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4); transform: translateY(-1px); }
.device-grid { .device-grid {
display: grid; display: grid;
@@ -1787,15 +1847,24 @@
<h2>Audit Log <span id="audit-count" class="device-count">0</span></h2> <h2>Audit Log <span id="audit-count" class="device-count">0</span></h2>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<button class="view-btn audit-filter-toggle" id="audit-filter-toggle" type="button"
aria-expanded="false" aria-controls="audit-filters"
onclick="toggleAuditFilters()">
<span class="label">Filters</span><span class="caret">&#9662;</span>
</button>
<button class="view-btn" onclick="queryAudit(0)" title="Reload newest records">Refresh</button> <button class="view-btn" onclick="queryAudit(0)" title="Reload newest records">Refresh</button>
</div> </div>
</div> </div>
<div class="audit-filters"> <div class="audit-filters collapsed" id="audit-filters">
<label>From <label>From
<input type="datetime-local" id="audit-from" onkeydown="if(event.key==='Enter')queryAudit(0)"> <span class="audit-date-wrap">
<input type="datetime-local" id="audit-from" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label> </label>
<label>To <label>To
<input type="datetime-local" id="audit-to" onkeydown="if(event.key==='Enter')queryAudit(0)"> <span class="audit-date-wrap">
<input type="datetime-local" id="audit-to" onkeydown="if(event.key==='Enter')queryAudit(0)">
</span>
</label> </label>
<label>Event (exact or prefix) <label>Event (exact or prefix)
<input type="text" id="audit-event" list="audit-event-options" placeholder="e.g. web.* or web.login.failure" <input type="text" id="audit-event" list="audit-event-options" placeholder="e.g. web.* or web.login.failure"
@@ -1913,6 +1982,23 @@
</div> </div>
</div> </div>
<!-- Generic alert / confirmation dialog. Replaces native alert()/confirm():
their button labels come from the browser locale (e.g. German on a
German system) and do not match the app. -->
<div class="modal-overlay" id="app-dialog" onclick="if(event.target===this)closeAppDialog(false)"
onkeydown="if(event.key==='Escape')closeAppDialog(false)">
<div class="confirm-modal-content" id="app-dialog-content" role="dialog" aria-modal="true"
aria-labelledby="app-dialog-title" aria-describedby="app-dialog-message">
<div class="confirm-modal-icon" id="app-dialog-icon"></div>
<h3 id="app-dialog-title">Notice</h3>
<p id="app-dialog-message"></p>
<div class="confirm-modal-actions">
<button class="cancel-btn" id="app-dialog-cancel" onclick="closeAppDialog(false)">Cancel</button>
<button class="danger-btn" id="app-dialog-ok" onclick="closeAppDialog(true)">OK</button>
</div>
</div>
</div>
<script src="__BASE_PATH__/static/xterm.js"></script> <script src="__BASE_PATH__/static/xterm.js"></script>
<script src="__BASE_PATH__/static/xterm-fit.js"></script> <script src="__BASE_PATH__/static/xterm-fit.js"></script>
@@ -2333,7 +2419,17 @@
wallAllocBudget(); wallAllocBudget();
getDevices(); getDevices();
} else { } else {
showPage('devices-page'); // Hiding the tab closes the socket on non-streaming
// pages and returning reconnects here. Keep an open
// audit log visible instead of yanking the admin back
// to the device list. Every other page still moves to
// devices: a terminal session died with the socket,
// and the login page needs to advance with a cached
// token.
const active = document.querySelector('.page.active');
if (!active || active.id !== 'audit-page') {
showPage('devices-page');
}
getDevices(); getDevices();
} }
} }
@@ -2642,7 +2738,7 @@
// mode=removed:历史记录已删除,刷新后卡片消失。 // mode=removed:历史记录已删除,刷新后卡片消失。
getDevices(); getDevices();
} else { } else {
alert(msg.msg || 'Failed to delete device'); appAlert(msg.msg || 'Failed to delete device', 'Unable to Delete Device');
} }
break; break;
case 'list_users_result': case 'list_users_result':
@@ -3228,7 +3324,10 @@
const existing = wall.sessions.get(l32); const existing = wall.sessions.get(l32);
if (existing && existing.fullId !== id) { if (existing && existing.fullId !== id) {
// 两台设备的低 32 位相同:绝不能共用一个解码器。 // 两台设备的低 32 位相同:绝不能共用一个解码器。
if (!restoring) alert('Device ID collision (low 32 bits). Cannot open both on the same wall.'); if (!restoring) {
appAlert('Device ID collision (low 32 bits). Cannot open both on the same wall.',
'Device ID Collision');
}
return null; return null;
} }
const uid = 'w' + (wall.uidSeq++); const uid = 'w' + (wall.uidSeq++);
@@ -3250,11 +3349,11 @@
function addWallTile(deviceId) { function addWallTile(deviceId) {
if (!serverCaps.multiWatch) { if (!serverCaps.multiWatch) {
alert('Screen Wall is not supported by this server'); appAlert('Screen Wall is not supported by this server.', 'Screen Wall Unavailable');
return; return;
} }
if (wall.tiles.length >= WALL_MAX_TILES) { if (wall.tiles.length >= WALL_MAX_TILES) {
alert('Wall is full (max ' + WALL_MAX_TILES + ' tiles)'); appAlert('Wall is full (max ' + WALL_MAX_TILES + ' tiles).', 'Screen Wall Full');
return; return;
} }
const dev = devices.find(d => String(d.id) === String(deviceId)); const dev = devices.find(d => String(d.id) === String(deviceId));
@@ -3294,9 +3393,11 @@
wallUpdateSidebarBadges(); wallUpdateSidebarBadges();
} }
function clearWall() { async function clearWall() {
if (!wall.tiles.length) return; if (!wall.tiles.length) return;
if (!confirm('Close all tiles on the wall?')) return; const ok = await appConfirm('Close all tiles on the wall?',
{ title: 'Clear Screen Wall', okText: 'Clear' });
if (!ok) return;
const tiles = wall.tiles.slice(); const tiles = wall.tiles.slice();
wall.tiles = []; wall.tiles = [];
for (const tile of tiles) { for (const tile of tiles) {
@@ -3508,8 +3609,8 @@
const tile = wall.tiles.find(t => t.uid === uid); const tile = wall.tiles.find(t => t.uid === uid);
if (!tile) return; if (!tile) return;
const dev = devices.find(d => String(d.id) === tile.deviceId); const dev = devices.find(d => String(d.id) === tile.deviceId);
if (!dev) { alert('Device not found'); return; } if (!dev) { appAlert('Device not found.', 'Device Not Found'); return; }
if (!dev.online) { alert('Device is offline'); return; } if (!dev.online) { appAlert('Device is offline.', 'Device Offline'); return; }
const s = wall.sessions.get(tile.low32); const s = wall.sessions.get(tile.low32);
if (s && s.status === 'offline') { if (s && s.status === 'offline') {
// 设备重新上线:离线时服务端已移除 watch,且 streams 条目被整体删除, // 设备重新上线:离线时服务端已移除 watch,且 streams 条目被整体删除,
@@ -3548,12 +3649,13 @@
function showWall() { function showWall() {
if (!serverCaps.multiWatch) { if (!serverCaps.multiWatch) {
alert('Screen Wall requires a newer server with multi-view support.'); appAlert('Screen Wall requires a newer server with multi-view support.',
'Screen Wall Unavailable');
return; return;
} }
const compat = checkWebCodecs(); const compat = checkWebCodecs();
if (!compat.supported) { if (!compat.supported) {
alert('Browser does not support H.264: ' + compat.reason); appAlert('Browser does not support H.264: ' + compat.reason, 'H.264 Not Supported');
return; return;
} }
if (!wallRestored) wallRestoreState(); if (!wallRestored) wallRestoreState();
@@ -4389,6 +4491,74 @@
updateAdminButtons(); updateAdminButtons();
} }
// ===== In-app alert / confirmation dialog =====
// Native alert()/confirm() use the browser locale for their buttons
// (e.g. "Abbrechen"/"OK" on a German system) and ignore the app theme.
// Both now go through the same themed modal as the logout prompt.
const APP_DIALOG_ICONS = {
danger: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>' +
'<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/>' +
'<line x1="12" y1="8" x2="12.01" y2="8"/></svg>'
};
let appDialogResolve = null;
function openAppDialog(opts) {
// A pending dialog is superseded (e.g. an async error arrives while
// a confirm is open): resolve it as cancelled so no caller hangs.
if (appDialogResolve) {
const previous = appDialogResolve;
appDialogResolve = null;
previous(false);
}
const danger = opts.danger !== false;
const content = document.getElementById('app-dialog-content');
const icon = document.getElementById('app-dialog-icon');
const cancel = document.getElementById('app-dialog-cancel');
const ok = document.getElementById('app-dialog-ok');
content.className = 'confirm-modal-content' + (danger ? '' : ' info');
icon.className = 'confirm-modal-icon' + (danger ? '' : ' info');
icon.innerHTML = danger ? APP_DIALOG_ICONS.danger : APP_DIALOG_ICONS.info;
document.getElementById('app-dialog-title').textContent =
opts.title || (danger ? 'Confirm' : 'Notice');
// textContent: server/device/user supplied strings must never be
// interpreted as markup.
document.getElementById('app-dialog-message').textContent = opts.message || '';
cancel.style.display = opts.cancelText ? '' : 'none';
cancel.textContent = opts.cancelText || 'Cancel';
ok.className = danger ? 'danger-btn' : 'primary-btn';
ok.textContent = opts.okText || 'OK';
document.getElementById('app-dialog').classList.add('active');
setTimeout(() => ok.focus(), 0);
return new Promise(resolve => { appDialogResolve = resolve; });
}
function closeAppDialog(result) {
document.getElementById('app-dialog').classList.remove('active');
const resolve = appDialogResolve;
appDialogResolve = null;
if (resolve) resolve(!!result);
}
// Fire-and-forget replacement for alert().
function appAlert(message, title) {
return openAppDialog({ title: title || 'Notice', message, okText: 'OK', danger: false });
}
// Promise<boolean> replacement for confirm().
function appConfirm(message, opts) {
opts = opts || {};
return openAppDialog({
title: opts.title || 'Confirm',
message,
okText: opts.okText || 'OK',
cancelText: opts.cancelText || 'Cancel',
danger: opts.danger !== false
});
}
// ===== 删除设备 ===== // ===== 删除设备 =====
// 两种情况都删除服务端 devices.json 中的历史记录;在线设备额外按 MFC // 两种情况都删除服务端 devices.json 中的历史记录;在线设备额外按 MFC
// 的做法发送单字节 TOKEN_UNINSTALL(卸载主机)。都先弹二次确认。 // 的做法发送单字节 TOKEN_UNINSTALL(卸载主机)。都先弹二次确认。
@@ -4402,7 +4572,7 @@
function askDeleteDevice(id) { function askDeleteDevice(id) {
if (!canDeleteDevices()) return; if (!canDeleteDevices()) return;
const d = devices.find(x => String(x.id) === String(id)); const d = devices.find(x => String(x.id) === String(id));
if (!d) { alert('Device not found'); return; } if (!d) { appAlert('Device not found.', 'Device Not Found'); return; }
const offline = d.online === false; const offline = d.online === false;
const name = displayName(d); const name = displayName(d);
pendingDeleteDeviceId = String(id); pendingDeleteDeviceId = String(id);
@@ -4498,8 +4668,10 @@
} }
} }
function deleteUser(username) { async function deleteUser(username) {
if (!confirm('Delete user "' + username + '"?')) return; const ok = await appConfirm('Delete user "' + username + '"?',
{ title: 'Delete User', okText: 'Delete' });
if (!ok) return;
if (ws && ws.readyState === WebSocket.OPEN && token) { if (ws && ws.readyState === WebSocket.OPEN && token) {
ws.send(JSON.stringify({ cmd: 'delete_user', token, username })); ws.send(JSON.stringify({ cmd: 'delete_user', token, username }));
} }
@@ -4542,6 +4714,20 @@
if (auditBtn) auditBtn.classList.toggle('visible', admin && !!serverCaps.auditLog); if (auditBtn) auditBtn.classList.toggle('visible', admin && !!serverCaps.auditLog);
} }
// Search conditions start collapsed (especially important on portrait
// phones); the toolbar button stays put and switches to a highlighted
// "up" state while the panel is open, meaning clicking again folds it.
function toggleAuditFilters() {
const panel = document.getElementById('audit-filters');
const btn = document.getElementById('audit-filter-toggle');
if (!panel || !btn) return;
const collapsed = panel.classList.toggle('collapsed');
btn.classList.toggle('open', !collapsed);
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
const label = btn.querySelector('.label');
if (label) label.textContent = collapsed ? 'Filters' : 'Hide filters';
}
function openAuditPage() { function openAuditPage() {
if (currentUserRole !== 'admin' || !serverCaps.auditLog) return; if (currentUserRole !== 'admin' || !serverCaps.auditLog) return;
fillAuditEventOptions(); fillAuditEventOptions();
@@ -4717,7 +4903,7 @@
function connectDevice(id, fromWall) { function connectDevice(id, fromWall) {
const dev = devices.find(d => d.id === id || d.id === String(id)); const dev = devices.find(d => d.id === id || d.id === String(id));
if (!dev || !dev.online) { if (!dev || !dev.online) {
alert('Device is offline'); appAlert('Device is offline.', 'Device Offline');
return; return;
} }
// 无显示器(screen 字段格式 "n:WxH",n=0 表示无显示器)→ 远程桌面没有意义, // 无显示器(screen 字段格式 "n:WxH",n=0 表示无显示器)→ 远程桌面没有意义,
@@ -4729,7 +4915,7 @@
return; return;
} }
const compat = checkWebCodecs(); const compat = checkWebCodecs();
if (!compat.supported) { alert('Browser does not support H264: ' + compat.reason); return; } if (!compat.supported) { appAlert('Browser does not support H264: ' + compat.reason, 'H.264 Not Supported'); return; }
const sid = String(id); const sid = String(id);
// 同页直接切设备(正常 UI 不会发生):先释放旧 focus 引用。 // 同页直接切设备(正常 UI 不会发生):先释放旧 focus 引用。
if (currentDevice && String(currentDevice.id) !== sid) { if (currentDevice && String(currentDevice.id) !== sid) {
@@ -4767,11 +4953,11 @@
function openTerminal(id) { function openTerminal(id) {
if (typeof Terminal === 'undefined') { if (typeof Terminal === 'undefined') {
alert('Terminal library not loaded yet, please retry'); appAlert('Terminal library not loaded yet, please retry.', 'Terminal Unavailable');
return; return;
} }
const dev = devices.find(d => d.id === id || d.id === String(id)); const dev = devices.find(d => d.id === id || d.id === String(id));
if (!dev || !dev.online) { alert('Device is offline'); return; } if (!dev || !dev.online) { appAlert('Device is offline.', 'Device Offline'); return; }
// 已经有终端在跑:直接 show(重连同设备视为重置) // 已经有终端在跑:直接 show(重连同设备视为重置)
if (termState.deviceId && termState.deviceId !== String(id)) { if (termState.deviceId && termState.deviceId !== String(id)) {
closeTerminal(); closeTerminal();
@@ -6317,6 +6503,8 @@
// Desktop keyboard handling // Desktop keyboard handling
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
if (!document.getElementById('screen-page').classList.contains('active')) return; if (!document.getElementById('screen-page').classList.contains('active')) return;
// Keys belong to an open in-app dialog (buttons are not INPUT/TEXTAREA).
if (document.getElementById('app-dialog').classList.contains('active')) return;
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// F11 belongs to the local fullscreen toggle UNLESS we're actively // F11 belongs to the local fullscreen toggle UNLESS we're actively
// controlling the remote — see the F11 handler above. Skip here // controlling the remote — see the F11 handler above. Skip here