Compare commits
6
Commits
ca671d6453
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
679136e73e | ||
|
|
b637c94e24 | ||
|
|
f76cd79bb0 | ||
|
|
39c8daa9b0 | ||
|
|
cd4f3675fd | ||
|
|
bbb3d8c2fa |
No files matched your search
@@ -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 扩展 & 国际化**
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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 擴充 & 國際化**
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
Binary file not shown.
+22
-4
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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.
@@ -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>
|
||||||
@@ -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 格式图片");
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 文字(大号艺术字体渲染)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
(已过期,续期从今天起算)= (已過期,續期從今天起算)
|
||||||
|
(原授权已过期,续期从今天起算)=(原授權已過期,續期從今天起算)
|
||||||
|
子目录=子目录
|
||||||
@@ -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
@@ -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">▾</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
|
||||||
|
|||||||
Reference in new issue
Block a user