Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d60e6e23 | ||
|
|
42df128f62 | ||
|
|
32cc7af371 | ||
|
|
033ff077ef |
No files matched your search
@@ -66,13 +66,15 @@ BOOL IsClientAppRunning(void* thisApp)
|
||||
return S_CLIENT_NORMAL == This->g_bExit;
|
||||
}
|
||||
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared)
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
|
||||
{
|
||||
auto v = StringToVector(remoteAddr, ':', 2);
|
||||
if (v[0].empty() || v[1].empty())
|
||||
return nullptr;
|
||||
auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
|
||||
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
|
||||
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
|
||||
a->g_Connection->iHeaderEnc = headerEnc;
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -597,6 +599,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
auto saved_ip = cfg.GetStr("settings", "master");
|
||||
auto saved_port = cfg.GetInt("settings", "port");
|
||||
settings.SetServer(saved_ip.c_str(), saved_port);
|
||||
auto saved_proto = cfg.GetStr("settings", "master_proto");
|
||||
if (saved_proto == "0" || saved_proto == "1")
|
||||
settings.iHeaderEnc = atoi(saved_proto.c_str());
|
||||
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
|
||||
int(valid_to-now));
|
||||
}
|
||||
@@ -617,7 +622,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
auto a = cfg.GetStr("settings", "share_list");
|
||||
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
|
||||
for (int i = 0; i < shareList.size(); ++i) {
|
||||
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE);
|
||||
auto v = StringToVector(shareList[i], ':', 3);
|
||||
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
|
||||
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
|
||||
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
|
||||
}
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE);
|
||||
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
|
||||
|
||||
// 启动核心线程,参数为:ClientApp
|
||||
DWORD WINAPI StartClient(LPVOID lParam);
|
||||
|
||||
@@ -1019,6 +1019,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
case COMMAND_SHARE:
|
||||
case COMMAND_ASSIGN_MASTER:
|
||||
if (ulLength > 2) {
|
||||
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
|
||||
int shareProto = -1;
|
||||
{
|
||||
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
|
||||
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
|
||||
shareProto = atoi(pv[2].c_str());
|
||||
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
|
||||
shareProto = atoi(pv[3].c_str());
|
||||
}
|
||||
switch (szBuffer[1]) {
|
||||
case SHARE_TYPE_YAMA_FOREVER: {
|
||||
auto v = StringToVector((char*)szBuffer + 2, ':', 3);
|
||||
@@ -1029,6 +1038,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (now <= valid_to) break; // Avoid assign again
|
||||
m_cfg->SetStr("settings", "master", v[0]);
|
||||
m_cfg->SetStr("settings", "port", v[1]);
|
||||
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
|
||||
float days = atof(v[2].c_str());
|
||||
if (days > 0) {
|
||||
auto valid_to = time(0) + days*86400;
|
||||
@@ -1047,6 +1058,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
if (v[0].empty() || v[1].empty())
|
||||
break;
|
||||
auto share = v[0] + ":" + v[1];
|
||||
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
|
||||
share += ":" + std::to_string(shareProto);
|
||||
auto list = m_cfg->GetStr("settings", "share_list");
|
||||
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
|
||||
if (VectorContains(shareList, share)) break;
|
||||
@@ -1054,7 +1067,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
|
||||
Mprintf("Share client to new master: %s\n", share.c_str());
|
||||
}
|
||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE);
|
||||
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
|
||||
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
|
||||
break;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -27,6 +27,7 @@
|
||||
#include <filesystem>
|
||||
#include "KeyBoardDlg.h"
|
||||
#include "InputDlg.h"
|
||||
#include "ShareHostDlg.h"
|
||||
#include "CPasswordDlg.h"
|
||||
#include "FeatureFlags.h"
|
||||
#include "FeatureLimitsDlg.h"
|
||||
@@ -43,6 +44,8 @@
|
||||
#include <sstream>
|
||||
#include "common/skCrypter.h"
|
||||
#include "common/commands.h"
|
||||
#include "QualityLevelsDlg.h"
|
||||
#include "CPreviewLevelsDlg.h"
|
||||
#include "McpServer.h"
|
||||
#include "McpSettingsDlg.h"
|
||||
#include "common/md5.h"
|
||||
@@ -1013,6 +1016,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_TOOL_IMPORT_LICENSE, &CMy2015RemoteDlg::OnToolImportLicense)
|
||||
ON_COMMAND(ID_TOOL_V2_PRIVATEKEY, &CMy2015RemoteDlg::OnToolV2PrivateKey)
|
||||
ON_COMMAND(ID_MENU_NOTIFY_SETTINGS, &CMy2015RemoteDlg::OnMenuNotifySettings)
|
||||
ON_COMMAND(ID_TOOL_QUALITY_LEVELS, &CMy2015RemoteDlg::OnQualityLevels)
|
||||
ON_COMMAND(ID_TOOL_PREVIEW_LEVELS, &CMy2015RemoteDlg::OnPreviewLevels)
|
||||
ON_COMMAND(ID_EXECUTE_TESTRUN, &CMy2015RemoteDlg::OnExecuteTestrun)
|
||||
ON_COMMAND(ID_EXECUTE_GHOST, &CMy2015RemoteDlg::OnExecuteGhost)
|
||||
ON_COMMAND(ID_MASTER_TRAIL, &CMy2015RemoteDlg::OnMasterTrail)
|
||||
@@ -2406,6 +2411,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
|
||||
// 这里读完 INI 后再 Apply 一次纠正过来。
|
||||
LoadThumbnailSettingsFromCfg();
|
||||
ApplyThumbnailSettings();
|
||||
LoadQualityProfilesFromCfg();
|
||||
LoadPreviewProfilesFromCfg();
|
||||
m_runNormal = THIS_CFG.GetInt("settings", "RunNormal", 0);
|
||||
|
||||
CMenu* SubMenu = m_MainMenu.GetSubMenu(2);
|
||||
@@ -7897,7 +7904,7 @@ LRESULT CMy2015RemoteDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineShare()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
CShareHostDlg dlg(this, FALSE);
|
||||
dlg.Init(_TR("分享主机"), _TR("输入<IP:PORT>地址:"));
|
||||
dlg.SetHistoryKey("ShareAddress");
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty())
|
||||
@@ -7906,9 +7913,10 @@ void CMy2015RemoteDlg::OnOnlineShare()
|
||||
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
CharMsg* buf = new CharMsg(dlg.m_str.GetLength()+1);
|
||||
memcpy(buf->data, dlg.m_str, dlg.m_str.GetLength());
|
||||
buf->data[dlg.m_str.GetLength()] = 0;
|
||||
CString payload = dlg.m_str + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
|
||||
CharMsg* buf = new CharMsg(payload.GetLength()+1);
|
||||
memcpy(buf->data, payload, payload.GetLength());
|
||||
buf->data[payload.GetLength()] = 0;
|
||||
PostMessageA(WM_SHARE_CLIENT, (WPARAM)buf, NULL);
|
||||
}
|
||||
|
||||
@@ -8794,7 +8802,7 @@ void CMy2015RemoteDlg::OnListSingleClick(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
OpenLoopTip(ctx, anchor);
|
||||
}
|
||||
|
||||
// 屏幕预览参数挑选:复用既有 GetTargetQualityLevel + GetScreenPreviewProfile
|
||||
// 屏幕预览参数挑选:复用 GetTargetQualityLevel + GetPreviewProfileCfg(可配置的 6 档预览参数表)
|
||||
// RTT 阈值表与屏幕共享共用(FRP 模式阈值更宽松);超清档(Ultra/High)+ 4K/超宽屏
|
||||
// 时按 min(screenWidth/4, 1280) 自适应放大,避免高分屏被压成无信息缩略图
|
||||
void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, BYTE& jpegQuality) const
|
||||
@@ -8808,7 +8816,7 @@ void CMy2015RemoteDlg::ChooseScreenPreviewParams(context* ctx, WORD& maxWidth, B
|
||||
int rttForLevel = ping > 0 ? ping : 250;
|
||||
|
||||
int level = GetTargetQualityLevel(rttForLevel, m_settings.UsingFRPProxy ? 1 : 0);
|
||||
const PreviewProfile& prof = GetScreenPreviewProfile(level);
|
||||
const PreviewProfile& prof = GetPreviewProfileCfg(level);
|
||||
int base = prof.maxWidth;
|
||||
BYTE q = (BYTE)prof.jpegQuality;
|
||||
|
||||
@@ -9142,6 +9150,118 @@ void CMy2015RemoteDlg::SaveThumbnailSettingsToCfg() const
|
||||
THIS_CFG.SetInt("thumbnail", "JpegQuality", m_ThumbnailCfg.JpegQuality);
|
||||
}
|
||||
|
||||
// ===== 远程桌面质量等级配置([quality] 节) =====
|
||||
// 6 档 QualityProfile 的读写与下发入口。缺省值回落到 common/commands.h 的
|
||||
// GetQualityProfile(),与客户端默认表一致;用户改过的档位单独落盘到 INI。
|
||||
|
||||
void CMy2015RemoteDlg::LoadQualityProfilesFromCfg()
|
||||
{
|
||||
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_QualityProfiles[i] = GetQualityProfile(i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
std::string sec = "profile" + std::to_string(i);
|
||||
QualityProfile& p = m_QualityProfiles[i];
|
||||
|
||||
p.maxFPS = THIS_CFG.GetInt("quality", sec + "_maxFPS", p.maxFPS);
|
||||
p.maxWidth = THIS_CFG.GetInt("quality", sec + "_maxWidth", p.maxWidth);
|
||||
p.algorithm = THIS_CFG.GetInt("quality", sec + "_algorithm", p.algorithm);
|
||||
p.bitRate = THIS_CFG.GetInt("quality", sec + "_bitRate", p.bitRate);
|
||||
|
||||
// Clamp 到安全范围(防 INI 被手工编错)
|
||||
if (p.maxFPS < 1) p.maxFPS = 1;
|
||||
if (p.maxFPS > 60) p.maxFPS = 60;
|
||||
if (p.maxWidth < 0) p.maxWidth = 0;
|
||||
if (p.maxWidth > 8192) p.maxWidth = 8192;
|
||||
if (p.algorithm < ALGORITHM_GRAY || p.algorithm > ALGORITHM_RGB565)
|
||||
p.algorithm = ALGORITHM_H264;
|
||||
if (p.bitRate < 0) p.bitRate = 0;
|
||||
if (p.bitRate > 50000) p.bitRate = 50000;
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SaveQualityProfilesToCfg() const
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
std::string sec = "profile" + std::to_string(i);
|
||||
const QualityProfile& p = m_QualityProfiles[i];
|
||||
THIS_CFG.SetInt("quality", sec + "_maxFPS", p.maxFPS);
|
||||
THIS_CFG.SetInt("quality", sec + "_maxWidth", p.maxWidth);
|
||||
THIS_CFG.SetInt("quality", sec + "_algorithm", p.algorithm);
|
||||
THIS_CFG.SetInt("quality", sec + "_bitRate", p.bitRate);
|
||||
}
|
||||
}
|
||||
|
||||
const QualityProfile& CMy2015RemoteDlg::GetQualityProfileCfg(int level) const
|
||||
{
|
||||
if (level < 0 || level >= QUALITY_COUNT)
|
||||
return GetQualityProfile(level); // 越界:与 common 默认表同源兜底
|
||||
return m_QualityProfiles[level];
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnQualityLevels()
|
||||
{
|
||||
CQualityLevelsDlg dlg(m_QualityProfiles, this);
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
SaveQualityProfilesToCfg();
|
||||
MessageBoxL("质量等级已保存,下次打开远程桌面时生效。", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 屏幕预览质量等级配置([preview] 节) =====
|
||||
// 6 档 PreviewProfile 的读写入口。缺省值回落到 common/commands.h 的
|
||||
// GetScreenPreviewProfile(),与内置预览参数表一致;用户改过的档位单独落盘到 INI。
|
||||
|
||||
void CMy2015RemoteDlg::LoadPreviewProfilesFromCfg()
|
||||
{
|
||||
// 先整体回落到内置默认,再逐字段覆盖,避免 INI 缺项时读到 0
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_PreviewProfiles[i] = GetScreenPreviewProfile(i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
std::string sec = "preview" + std::to_string(i);
|
||||
PreviewProfile& p = m_PreviewProfiles[i];
|
||||
|
||||
p.maxWidth = THIS_CFG.GetInt("preview", sec + "_maxWidth", p.maxWidth);
|
||||
p.jpegQuality = THIS_CFG.GetInt("preview", sec + "_jpegQuality", p.jpegQuality);
|
||||
|
||||
// Clamp 到安全范围(防 INI 被手工编错)
|
||||
if (p.maxWidth < 64) p.maxWidth = 64;
|
||||
if (p.maxWidth > 2560) p.maxWidth = 2560;
|
||||
if (p.jpegQuality < 1) p.jpegQuality = 1;
|
||||
if (p.jpegQuality > 100) p.jpegQuality = 100;
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SavePreviewProfilesToCfg() const
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
std::string sec = "preview" + std::to_string(i);
|
||||
const PreviewProfile& p = m_PreviewProfiles[i];
|
||||
THIS_CFG.SetInt("preview", sec + "_maxWidth", p.maxWidth);
|
||||
THIS_CFG.SetInt("preview", sec + "_jpegQuality", p.jpegQuality);
|
||||
}
|
||||
}
|
||||
|
||||
const PreviewProfile& CMy2015RemoteDlg::GetPreviewProfileCfg(int level) const
|
||||
{
|
||||
if (level < 0 || level >= QUALITY_COUNT)
|
||||
return GetScreenPreviewProfile(level); // 越界:与 common 默认表同源兜底
|
||||
return m_PreviewProfiles[level];
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnPreviewLevels()
|
||||
{
|
||||
CPreviewLevelsDlg dlg(m_PreviewProfiles, this);
|
||||
if (dlg.DoModal() == IDOK) {
|
||||
SavePreviewProfilesToCfg();
|
||||
MessageBoxL("预览质量等级已保存,下次发送预览时生效。", "提示", MB_ICONINFORMATION);
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ApplyThumbnailSettings()
|
||||
{
|
||||
// 任何已缓存的 HBITMAP 都是按"旧 ThumbWidth"渲染的,尺寸不再可信 —— 一律清空。
|
||||
@@ -9661,26 +9781,27 @@ void CMy2015RemoteDlg::OnShellcodeTestAesBin()
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineAssignTo()
|
||||
{
|
||||
CInputDialog dlg(this);
|
||||
CShareHostDlg dlg(this, TRUE);
|
||||
dlg.Init(_TR("转移主机(到期自动复原)"), _TR("输入<IP:PORT>地址:"));
|
||||
dlg.SetHistoryKey("ShareAddress");
|
||||
dlg.Init2(_TR("天数(支持浮点数):"), "30");
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sSecondInput.GetString())<=0)
|
||||
dlg.InitDays(_TR("天数(支持浮点数):"), "30");
|
||||
if (dlg.DoModal() != IDOK || dlg.m_str.IsEmpty() || atof(dlg.m_sDays.GetString())<=0)
|
||||
return;
|
||||
if (dlg.m_str.GetLength() >= 250) {
|
||||
MessageBoxL("字符串长度超出[0, 250]范围限制!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
if (dlg.m_sSecondInput.GetLength() >= 6) {
|
||||
if (dlg.m_sDays.GetLength() >= 6) {
|
||||
MessageBoxL("超出使用时间可输入的字符数限制!", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
CString daysProto = dlg.m_sDays + _T(":") + (dlg.m_bShine ? _T("0") : _T("1"));
|
||||
CharMsg* buf1 = new CharMsg(dlg.m_str.GetLength() + 1);
|
||||
CharMsg* buf2 = new CharMsg(dlg.m_sSecondInput.GetLength() + 1);
|
||||
CharMsg* buf2 = new CharMsg(daysProto.GetLength() + 1);
|
||||
memcpy(buf1->data, dlg.m_str, dlg.m_str.GetLength());
|
||||
memcpy(buf2->data, dlg.m_sSecondInput, dlg.m_sSecondInput.GetLength());
|
||||
memcpy(buf2->data, daysProto, daysProto.GetLength());
|
||||
buf1->data[dlg.m_str.GetLength()] = 0;
|
||||
buf2->data[dlg.m_sSecondInput.GetLength()] = 0;
|
||||
buf2->data[daysProto.GetLength()] = 0;
|
||||
PostMessageA(WM_ASSIGN_CLIENT, (WPARAM)buf1, (LPARAM)buf2);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "TrueColorToolBar.h"
|
||||
#include "IOCPServer.h"
|
||||
#include <common/location.h>
|
||||
#include "common/commands.h"
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
@@ -328,6 +329,24 @@ public:
|
||||
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
|
||||
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
|
||||
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
|
||||
|
||||
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
|
||||
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
|
||||
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
|
||||
QualityProfile m_QualityProfiles[QUALITY_COUNT];
|
||||
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
|
||||
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
|
||||
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
|
||||
void OnQualityLevels(); // 高级→远程桌面质量等级
|
||||
|
||||
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
|
||||
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
|
||||
// 仅 UI 线程访问;修改后下次发预览请求即生效。
|
||||
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
|
||||
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
|
||||
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
|
||||
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
|
||||
void OnPreviewLevels(); // 高级→屏幕预览质量等级
|
||||
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
|
||||
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
|
||||
void ClearThumbnailCacheEntry(uint64_t clientID);
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
<ClInclude Include="NotifySettingsDlg.h" />
|
||||
<ClInclude Include="FeatureLimitsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="ShareHostDlg.h" />
|
||||
<ClInclude Include="PluginSettingsDlg.h" />
|
||||
<ClInclude Include="PreviewTipWnd.h" />
|
||||
<ClInclude Include="TriggerSettingsDlg.h" />
|
||||
@@ -462,6 +463,7 @@
|
||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||
<ClCompile Include="ShareHostDlg.cpp" />
|
||||
<ClCompile Include="Loader.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<ClCompile Include="NotifyManager.cpp" />
|
||||
<ClCompile Include="NotifySettingsDlg.cpp" />
|
||||
<ClCompile Include="FrpsForSubDlg.cpp" />
|
||||
<ClCompile Include="ShareHostDlg.cpp" />
|
||||
<ClCompile Include="NetworkDlg.cpp" />
|
||||
<ClCompile Include="IPHistoryDlg.cpp" />
|
||||
<ClCompile Include="FeatureLimitsDlg.cpp" />
|
||||
@@ -181,6 +182,7 @@
|
||||
<ClInclude Include="NotifyManager.h" />
|
||||
<ClInclude Include="NotifySettingsDlg.h" />
|
||||
<ClInclude Include="FrpsForSubDlg.h" />
|
||||
<ClInclude Include="ShareHostDlg.h" />
|
||||
<ClInclude Include="TerminalModuleLoader.h" />
|
||||
<ClInclude Include="..\..\common\SafeString.h" />
|
||||
<ClInclude Include="NetworkDlg.h" />
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// CPreviewLevelsDlg.h
|
||||
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
|
||||
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
|
||||
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
|
||||
#pragma once
|
||||
|
||||
#include "LangManager.h"
|
||||
|
||||
class CPreviewLevelsDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
enum { IDD = IDD_PREVIEW_LEVELS };
|
||||
|
||||
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
|
||||
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
|
||||
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
PreviewProfile* m_profiles;
|
||||
PreviewProfile m_working[QUALITY_COUNT];
|
||||
|
||||
static const char* LevelName(int i)
|
||||
{
|
||||
static const char* names[QUALITY_COUNT] = {
|
||||
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
|
||||
};
|
||||
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
|
||||
}
|
||||
|
||||
void LoadToControls()
|
||||
{
|
||||
CString s;
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
s.Format(_T("%d"), m_working[i].maxWidth);
|
||||
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
|
||||
s.Format(_T("%d"), m_working[i].jpegQuality);
|
||||
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadInt(int id, int& out)
|
||||
{
|
||||
CString s;
|
||||
GetDlgItemText(id, s);
|
||||
s.Trim();
|
||||
if (s.IsEmpty()) return false;
|
||||
|
||||
int v = 0;
|
||||
for (int k = 0; k < s.GetLength(); k++) {
|
||||
TCHAR ch = s[k];
|
||||
if (ch < _T('0') || ch > _T('9')) return false;
|
||||
v = v * 10 + (ch - _T('0'));
|
||||
}
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadFromControls(CString& err)
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
int w, q;
|
||||
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
|
||||
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
|
||||
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
|
||||
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
|
||||
|
||||
if (w < 64 || w > 2560)
|
||||
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
|
||||
if (q < 1 || q > 100)
|
||||
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
|
||||
|
||||
m_working[i].maxWidth = w;
|
||||
m_working[i].jpegQuality = q;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResetToDefaults()
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = GetScreenPreviewProfile(i);
|
||||
}
|
||||
LoadToControls();
|
||||
}
|
||||
|
||||
virtual BOOL OnInitDialog() override
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = m_profiles[i];
|
||||
}
|
||||
LoadToControls();
|
||||
TranslateDialog(this);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
virtual void OnOK() override
|
||||
{
|
||||
CString err;
|
||||
if (!ReadFromControls(err)) {
|
||||
MessageBox(err, _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_profiles[i] = m_working[i];
|
||||
}
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||
{
|
||||
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||
ResetToDefaults();
|
||||
return TRUE;
|
||||
}
|
||||
return CDialog::OnCommand(wParam, lParam);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
// QualityLevelsDlg.h
|
||||
// 远程桌面质量等级配置对话框:编辑 6 档 QualityProfile(帧率/宽度/算法/码率)。
|
||||
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SaveQualityProfilesToCfg() 负责。
|
||||
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
|
||||
#pragma once
|
||||
|
||||
#include "LangManager.h"
|
||||
|
||||
class CQualityLevelsDlg : public CDialog
|
||||
{
|
||||
public:
|
||||
enum { IDD = IDD_QUALITY_LEVELS };
|
||||
|
||||
// profiles: 指向 6 元素 QualityProfile 数组,确认后写回
|
||||
CQualityLevelsDlg(QualityProfile* profiles, CWnd* pParent = NULL)
|
||||
: CDialog(CQualityLevelsDlg::IDD, pParent), m_profiles(profiles)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
QualityProfile* m_profiles;
|
||||
QualityProfile m_working[QUALITY_COUNT];
|
||||
|
||||
static const char* AlgorithmName(int algo)
|
||||
{
|
||||
switch (algo) {
|
||||
case ALGORITHM_GRAY: return "GRAY";
|
||||
case ALGORITHM_DIFF: return "DIFF";
|
||||
case ALGORITHM_H264: return "H264";
|
||||
case ALGORITHM_RGB565: return "RGB565";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static const char* LevelName(int i)
|
||||
{
|
||||
static const char* names[QUALITY_COUNT] = {
|
||||
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
|
||||
};
|
||||
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
|
||||
}
|
||||
|
||||
void FillAlgoCombo(int i)
|
||||
{
|
||||
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
|
||||
if (!cb) return;
|
||||
for (int a = 0; a < 4; a++) {
|
||||
cb->AddString(CString(AlgorithmName(a)));
|
||||
}
|
||||
cb->SetCurSel(m_working[i].algorithm);
|
||||
}
|
||||
|
||||
void LoadToControls()
|
||||
{
|
||||
CString s;
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
s.Format(_T("%d"), m_working[i].maxFPS);
|
||||
SetDlgItemText(IDC_QLEVEL_FPS + i, s);
|
||||
s.Format(_T("%d"), m_working[i].maxWidth);
|
||||
SetDlgItemText(IDC_QLEVEL_WIDTH + i, s);
|
||||
s.Format(_T("%d"), m_working[i].bitRate);
|
||||
SetDlgItemText(IDC_QLEVEL_BITRATE + i, s);
|
||||
FillAlgoCombo(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadInt(int id, int& out)
|
||||
{
|
||||
CString s;
|
||||
GetDlgItemText(id, s);
|
||||
s.Trim();
|
||||
if (s.IsEmpty()) return false;
|
||||
|
||||
int v = 0;
|
||||
for (int k = 0; k < s.GetLength(); k++) {
|
||||
TCHAR ch = s[k];
|
||||
if (ch < _T('0') || ch > _T('9')) return false;
|
||||
v = v * 10 + (ch - _T('0'));
|
||||
}
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadFromControls(CString& err)
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
int fps, w, br;
|
||||
if (!ReadInt(IDC_QLEVEL_FPS + i, fps))
|
||||
{ err.Format(_T("等级 %s 的帧率不是有效数字"), LevelName(i)); return false; }
|
||||
if (!ReadInt(IDC_QLEVEL_WIDTH + i, w))
|
||||
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
|
||||
if (!ReadInt(IDC_QLEVEL_BITRATE + i, br))
|
||||
{ err.Format(_T("等级 %s 的码率不是有效数字"), LevelName(i)); return false; }
|
||||
|
||||
if (fps < 1 || fps > 60)
|
||||
{ err.Format(_T("等级 %s 的帧率需在 1~60 之间"), LevelName(i)); return false; }
|
||||
if (w < 0 || w > 8192)
|
||||
{ err.Format(_T("等级 %s 的最大宽度需在 0~8192 之间(0=原始分辨率)"), LevelName(i)); return false; }
|
||||
if (br < 0 || br > 50000)
|
||||
{ err.Format(_T("等级 %s 的码率需在 0~50000 之间"), LevelName(i)); return false; }
|
||||
|
||||
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
|
||||
int algo = cb ? cb->GetCurSel() : -1;
|
||||
if (algo < 0 || algo > 3)
|
||||
{ err.Format(_T("等级 %s 的编码算法未选择"), LevelName(i)); return false; }
|
||||
|
||||
m_working[i].maxFPS = fps;
|
||||
m_working[i].maxWidth = w;
|
||||
m_working[i].bitRate = br;
|
||||
m_working[i].algorithm = algo;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResetToDefaults()
|
||||
{
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = GetQualityProfile(i);
|
||||
}
|
||||
LoadToControls();
|
||||
}
|
||||
|
||||
virtual BOOL OnInitDialog() override
|
||||
{
|
||||
CDialog::OnInitDialog();
|
||||
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_working[i] = m_profiles[i];
|
||||
}
|
||||
LoadToControls();
|
||||
TranslateDialog(this);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
virtual void OnOK() override
|
||||
{
|
||||
CString err;
|
||||
if (!ReadFromControls(err)) {
|
||||
MessageBox(err, _TR("提示"), MB_ICONWARNING);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
m_profiles[i] = m_working[i];
|
||||
}
|
||||
CDialog::OnOK();
|
||||
}
|
||||
|
||||
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
|
||||
{
|
||||
if (LOWORD(wParam) == IDC_QLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
|
||||
ResetToDefaults();
|
||||
return TRUE;
|
||||
}
|
||||
return CDialog::OnCommand(wParam, lParam);
|
||||
}
|
||||
};
|
||||
@@ -889,13 +889,17 @@ BOOL CScreenSpyDlg::OnInitDialog()
|
||||
// 启动传输速率更新定时器 (1秒)
|
||||
SetTimer(4, 1000, NULL);
|
||||
|
||||
// 下发质量配置表(让客户端可以持久化保存)
|
||||
// 下发质量配置表(让客户端可以持久化保存)。
|
||||
// 优先使用主控端在「高级 → 远程桌面质量等级」里自定义的档位;无主控端时回落内置默认表。
|
||||
{
|
||||
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
|
||||
profileCmd[0] = CMD_QUALITY_PROFILES;
|
||||
for (int i = 0; i < QUALITY_COUNT; i++) {
|
||||
const QualityProfile& prof = (m_pParent != nullptr)
|
||||
? m_pParent->GetQualityProfileCfg(i)
|
||||
: GetQualityProfile(i);
|
||||
memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
|
||||
&GetQualityProfile(i), sizeof(QualityProfile));
|
||||
&prof, sizeof(QualityProfile));
|
||||
}
|
||||
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// ShareHostDlg.cpp: 分享主机 / 转移主机 专用对话框实现
|
||||
//
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ShareHostDlg.h"
|
||||
#include "2015Remote.h"
|
||||
#include <algorithm>
|
||||
|
||||
CShareHostDlg::CShareHostDlg(CWnd* pParent /*=nullptr*/, BOOL bNeedDays /*=FALSE*/)
|
||||
: CDialogLangEx(IDD_DIALOG_SHARE_HOST, pParent)
|
||||
, m_bNeedDays(bNeedDays)
|
||||
, m_bShine(TRUE)
|
||||
{
|
||||
}
|
||||
|
||||
CShareHostDlg::~CShareHostDlg()
|
||||
{
|
||||
}
|
||||
|
||||
void CShareHostDlg::Init(LPCTSTR caption, LPCTSTR prompt)
|
||||
{
|
||||
m_sCaption = caption;
|
||||
m_sPrompt = prompt;
|
||||
}
|
||||
|
||||
void CShareHostDlg::InitDays(LPCTSTR name, LPCTSTR defaultValue)
|
||||
{
|
||||
m_sDaysName = name;
|
||||
m_sDays = defaultValue;
|
||||
}
|
||||
|
||||
void CShareHostDlg::SetHistoryKey(LPCTSTR historyKey)
|
||||
{
|
||||
m_sHistoryKey = historyKey;
|
||||
}
|
||||
|
||||
void CShareHostDlg::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogLangEx::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_EDIT_SHARE_ADDRESS, m_ComboInput);
|
||||
DDX_Control(pDX, IDC_STATIC_SHARE_DAYS, m_StaticDays);
|
||||
DDX_Control(pDX, IDC_EDIT_SHARE_DAYS, m_EditDays);
|
||||
DDX_Control(pDX, IDC_CHECK_USE_SHINE, m_CheckShine);
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CShareHostDlg, CDialogLangEx)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
BOOL CShareHostDlg::OnInitDialog()
|
||||
{
|
||||
CDialogLangEx::OnInitDialog();
|
||||
|
||||
SetWindowText(m_sCaption);
|
||||
SetDlgItemText(IDC_STATIC_SHARE_ADDRESS, m_sPrompt);
|
||||
SetDlgItemText(IDC_CHECK_USE_SHINE, _TR("使用 Shine 协议(兼容 Go 服务端)"));
|
||||
SetDlgItemText(IDOK, _TR("确定"));
|
||||
SetDlgItemText(IDCANCEL, _TR("取消"));
|
||||
|
||||
// 天数(仅转移主机显示)
|
||||
m_StaticDays.SetWindowTextA(m_sDaysName);
|
||||
m_StaticDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
|
||||
m_EditDays.SetWindowTextA(m_sDays);
|
||||
m_EditDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
|
||||
|
||||
// 默认勾选 Shine
|
||||
m_CheckShine.SetCheck(m_bShine ? BST_CHECKED : BST_UNCHECKED);
|
||||
|
||||
LoadHistory();
|
||||
m_ComboInput.SetWindowText(m_str);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CShareHostDlg::OnOK()
|
||||
{
|
||||
m_ComboInput.GetWindowText(m_str);
|
||||
m_bShine = (m_CheckShine.GetCheck() == BST_CHECKED);
|
||||
if (m_bNeedDays)
|
||||
m_EditDays.GetWindowText(m_sDays);
|
||||
|
||||
SaveHistory();
|
||||
CDialogLangEx::OnOK();
|
||||
}
|
||||
|
||||
void CShareHostDlg::LoadHistory()
|
||||
{
|
||||
if (m_sHistoryKey.IsEmpty()) return;
|
||||
|
||||
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
|
||||
if (history.empty()) return;
|
||||
|
||||
auto items = StringToVector(history, '|');
|
||||
for (size_t i = 0; i < items.size() && i < 16; i++) {
|
||||
if (!items[i].empty()) {
|
||||
m_ComboInput.AddString(items[i].c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CShareHostDlg::SaveHistory()
|
||||
{
|
||||
if (m_sHistoryKey.IsEmpty() || m_str.IsEmpty()) return;
|
||||
|
||||
std::string newValue = m_str.GetString();
|
||||
if (newValue.find('|') != std::string::npos) return;
|
||||
|
||||
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
|
||||
auto items = StringToVector(history, '|');
|
||||
|
||||
items.erase(std::remove(items.begin(), items.end(), newValue), items.end());
|
||||
items.erase(std::remove(items.begin(), items.end(), std::string("")), items.end());
|
||||
|
||||
items.insert(items.begin(), newValue);
|
||||
|
||||
if (items.size() > 16) {
|
||||
items.resize(16);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
for (size_t i = 0; i < items.size(); i++) {
|
||||
if (i > 0) result += '|';
|
||||
result += items[i];
|
||||
}
|
||||
THIS_CFG.SetStr("history", m_sHistoryKey.GetString(), result);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
#include "LangManager.h"
|
||||
|
||||
// 分享主机 / 转移主机 专用对话框(带 Shine/HELL 协议勾选)
|
||||
|
||||
class CShareHostDlg : public CDialogLangEx
|
||||
{
|
||||
public:
|
||||
CShareHostDlg(CWnd* pParent = nullptr, BOOL bNeedDays = FALSE);
|
||||
virtual ~CShareHostDlg();
|
||||
|
||||
enum { IDD = IDD_DIALOG_SHARE_HOST };
|
||||
|
||||
void Init(LPCTSTR caption, LPCTSTR prompt);
|
||||
void InitDays(LPCTSTR name, LPCTSTR defaultValue); // 仅转移主机显示
|
||||
void SetHistoryKey(LPCTSTR historyKey);
|
||||
|
||||
// 结果
|
||||
CString m_str; // 地址 IP:PORT
|
||||
CString m_sDays; // 天数(转移主机)
|
||||
BOOL m_bShine; // TRUE=Shine(0), FALSE=HELL(1)
|
||||
|
||||
protected:
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void OnOK();
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
CString m_sCaption;
|
||||
CString m_sPrompt;
|
||||
CString m_sDaysName;
|
||||
CString m_sHistoryKey;
|
||||
BOOL m_bNeedDays;
|
||||
|
||||
CComboBox m_ComboInput;
|
||||
CStatic m_StaticDays;
|
||||
CEdit m_EditDays;
|
||||
CButton m_CheckShine;
|
||||
|
||||
void LoadHistory();
|
||||
void SaveHistory();
|
||||
};
|
||||
@@ -35,6 +35,16 @@ static std::string LoadBinaryResourceAsString(int resourceId) {
|
||||
return std::string(static_cast<const char*>(p), size);
|
||||
}
|
||||
|
||||
// Replace all occurrences of `from` with `to` in-place.
|
||||
static void ReplaceAll(std::string& s, const std::string& from, const std::string& to) {
|
||||
if (from.empty()) return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.length(), to);
|
||||
pos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
// Challenge-response nonce storage (prevents replay attacks)
|
||||
static std::map<void*, std::string> s_ClientNonces;
|
||||
static std::mutex s_NonceMutex;
|
||||
@@ -231,6 +241,10 @@ void CWebService::ServerThread(int port) {
|
||||
|
||||
// Serve static HTML page and file downloads
|
||||
static std::string cachedHtml = GetWebPageHTML();
|
||||
// server/web/index.html carries a __BASE_PATH__ placeholder for the Go
|
||||
// server's subpath deployment. The MFC server serves from the domain root,
|
||||
// so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break.
|
||||
ReplaceAll(cachedHtml, "__BASE_PATH__", "");
|
||||
// Log loaded size; zero bytes means the RC BINARY resource is missing or
|
||||
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
|
||||
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
|
||||
|
||||
@@ -2025,3 +2025,25 @@ Token=Token
|
||||
打开文件失败...=Datei konnte nicht geoeffnet werden...
|
||||
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=Diese Aufgabe startet bei Bedarf den Windows-Update-Dienst zum Ausfuehren geplanter Vorgaenge (z.B. Scannen)
|
||||
本地文件路径=Lokaler Dateipfad
|
||||
高级(&A)=&Erweitert
|
||||
远程桌面质量等级(&Q)=Remote Desktop &Qualitaetsstufen
|
||||
远程桌面质量等级=Remote Desktop Qualitaetsstufen
|
||||
等级=Stufe
|
||||
帧率(FPS)=FPS
|
||||
最大宽度=Max. Breite
|
||||
编码算法=Codec
|
||||
码率(kbps)=Bitrate (kbps)
|
||||
Ultra 极佳=Ultra
|
||||
High 优秀=Hoch
|
||||
Good 良好=Gut
|
||||
Medium 一般=Mittel
|
||||
Low 较差=Niedrig
|
||||
Minimal 最低=Minimal
|
||||
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=Max. Breite 0 = Originalaufloesung; DIFF/RGB565 fuer LAN, H264 fuer WAN
|
||||
恢复默认=Standard
|
||||
质量等级已保存,下次打开远程桌面时生效。=Qualitaetsstufen gespeichert. Wirksam beim naechsten Oeffnen des Remote Desktops.
|
||||
屏幕预览质量等级(&P)=Vorschau &Qualitaetsstufen
|
||||
屏幕预览质量等级=Vorschau Qualitaetsstufen
|
||||
JPEG质量=JPEG Qualitaet
|
||||
最大宽度为期望编码宽度;JPEG 质量 1~100=Max. Breite ist die Ziel-Codierbreite; JPEG Qualitaet 1~100
|
||||
预览质量等级已保存,下次发送预览时生效。=Vorschau-Qualitaetsstufen gespeichert. Wirksam bei der naechsten Vorschau.
|
||||
@@ -390,6 +390,7 @@ Rar
|
||||
客户管理=Client Mgmt
|
||||
转移主机=Transfer Host
|
||||
分享主机=Share Host
|
||||
使用 Shine 协议(兼容 Go 服务端)=Use Shine protocol (Go-server compatible)
|
||||
取消分享=Cancel Sharing
|
||||
确定取消分享主机? 将撤销全部的分享链接。=Cancel sharing clients? This will remove all share links.
|
||||
删除主机=Delete Host
|
||||
@@ -1353,7 +1354,7 @@ FUCK!!!
|
||||
编辑备注=Edit remarks
|
||||
自己:=Self:
|
||||
视频文件(*.avi)|*.avi|=Video(*.avi)|*.avi|
|
||||
½ג³�ר¶¨�ה�כ=½ג³�ר¶¨�ה�כ
|
||||
解除锁定输入=Unlock Remote Input
|
||||
计算机已经被用户锁屏,程序无 SYSTEM 权限。\r\n可能无法进行远程桌面控制,是否继续?=Computer locked, no SYSTEM.\r\nRemote may fail, continue?
|
||||
请先生成口令!=Generate password first!
|
||||
请将EXE放在\=Put EXE in \
|
||||
@@ -2025,3 +2026,25 @@ Token=Token
|
||||
打开文件失败...=Failed to open file...
|
||||
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=This task starts the Windows Update service when needed to run scheduled operations (e.g. scanning)
|
||||
本地文件路径=Local file path
|
||||
高级(&A)=&Advanced
|
||||
远程桌面质量等级(&Q)=Remote Desktop &Quality Levels
|
||||
远程桌面质量等级=Remote Desktop Quality Levels
|
||||
等级=Level
|
||||
帧率(FPS)=FPS
|
||||
最大宽度=Max Width
|
||||
编码算法=Codec
|
||||
码率(kbps)=Bitrate (kbps)
|
||||
Ultra 极佳=Ultra
|
||||
High 优秀=High
|
||||
Good 良好=Good
|
||||
Medium 一般=Medium
|
||||
Low 较差=Low
|
||||
Minimal 最低=Minimal
|
||||
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=Max Width 0 = native resolution; DIFF/RGB565 for LAN, H264 for WAN
|
||||
恢复默认=Restore Defaults
|
||||
质量等级已保存,下次打开远程桌面时生效。=Quality levels saved. They take effect the next time you open Remote Desktop.
|
||||
屏幕预览质量等级(&P)=Screen Preview &Quality Levels
|
||||
屏幕预览质量等级=Screen Preview Quality Levels
|
||||
JPEG质量=JPEG Quality
|
||||
最大宽度为期望编码宽度;JPEG 质量 1~100=Max width is the target encode width; JPEG quality 1~100
|
||||
预览质量等级已保存,下次发送预览时生效。=Preview quality levels saved. They take effect the next time a preview is sent.
|
||||
@@ -390,6 +390,7 @@ Rar
|
||||
客户管理=用戶管理
|
||||
转移主机=轉移主機
|
||||
分享主机=分享主機
|
||||
使用 Shine 协议(兼容 Go 服务端)=使用 Shine 協議(相容 Go 伺服器)
|
||||
取消分享=取消分享
|
||||
确定取消分享主机? 将撤销全部的分享链接。=确定取消分享主机? 将撤销全部的分享链接。
|
||||
删除主机=刪除主機
|
||||
@@ -2016,3 +2017,25 @@ Token=Token
|
||||
打开文件失败...=開啟檔案失敗...
|
||||
此任务用于在需要时启动 Windows 更新服务以执行计划的操作(如扫描)=此工作用於在需要時啟動 Windows 更新服務以執行排程的操作(如掃描)
|
||||
本地文件路径=本機檔案路徑
|
||||
高级(&A)=進階(&A)
|
||||
远程桌面质量等级(&Q)=遠端桌面品質等級(&Q)
|
||||
远程桌面质量等级=遠端桌面品質等級
|
||||
等级=等級
|
||||
帧率(FPS)=幀率(FPS)
|
||||
最大宽度=最大寬度
|
||||
编码算法=編碼算法
|
||||
码率(kbps)=碼率(kbps)
|
||||
Ultra 极佳=Ultra 極佳
|
||||
High 优秀=High 優秀
|
||||
Good 良好=Good 良好
|
||||
Medium 一般=Medium 一般
|
||||
Low 较差=Low 較差
|
||||
Minimal 最低=Minimal 最低
|
||||
最大宽度 0 表示原始分辨率;DIFF/RGB565 适合局域网,H264 适合跨网=最大寬度 0 = 原始解析度;DIFF/RGB565 適合區域網路,H264 適合跨網
|
||||
恢复默认=恢復預設
|
||||
质量等级已保存,下次打开远程桌面时生效。=品質等級已儲存,下次開啟遠端桌面時生效。
|
||||
屏幕预览质量等级(&P)=螢幕預覽品質等級(&P)
|
||||
屏幕预览质量等级=螢幕預覽品質等級
|
||||
JPEG质量=JPEG品質
|
||||
最大宽度为期望编码宽度;JPEG 质量 1~100=最大寬度為期望編碼寬度;JPEG 品質 1~100
|
||||
预览质量等级已保存,下次发送预览时生效。=預覽品質等級已儲存,下次傳送預覽時生效。
|
||||
@@ -353,6 +353,8 @@
|
||||
#define IDI_SYSTEM 2035
|
||||
#define IDC_DOT 2036
|
||||
#define IDD_AUDIO 2038
|
||||
#define IDD_QUALITY_LEVELS 3000
|
||||
#define IDD_PREVIEW_LEVELS 3001
|
||||
#define IDI_AUDIO 2039
|
||||
#define IDC_LIST_REMOTE 2040
|
||||
#define IDC_LIST_LOCAL 2041
|
||||
@@ -1036,14 +1038,31 @@
|
||||
#define ID_ONLINE_FORBIDDEN 33076
|
||||
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
#define ID_TOOL_QUALITY_LEVELS 33083
|
||||
#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
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 401
|
||||
#define _APS_NEXT_COMMAND_VALUE 33083
|
||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||
#define _APS_NEXT_RESOURCE_VALUE 403
|
||||
#define _APS_NEXT_COMMAND_VALUE 33085
|
||||
#define _APS_NEXT_CONTROL_VALUE 2590
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
#endif
|
||||
@@ -661,6 +661,19 @@ func splitCSV(s string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizeBasePath turns a YAMA_WEB_BASE_PATH value into a leading-slash,
|
||||
// no-trailing-slash prefix ("" for root). "" (or "/") means the web UI is
|
||||
// served at the domain root (方案A); "/remoter" means it is served under that
|
||||
// prefix (方案B), with a reverse proxy routing the subpath to this process.
|
||||
func normalizeBasePath(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.Trim(s, "/")
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return "/" + s
|
||||
}
|
||||
|
||||
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
|
||||
// signed with the operator's RSA private key and prints it to stdout.
|
||||
//
|
||||
@@ -918,11 +931,16 @@ func main() {
|
||||
if trustProxy {
|
||||
log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front")
|
||||
}
|
||||
basePath := normalizeBasePath(os.Getenv("YAMA_WEB_BASE_PATH"))
|
||||
if basePath != "" {
|
||||
log.Info("Serving web UI under base path %q (subpath deployment)", basePath)
|
||||
}
|
||||
webCfg := web.Config{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute),
|
||||
LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute),
|
||||
TrustForwardedFor: trustProxy,
|
||||
BasePath: basePath,
|
||||
}
|
||||
|
||||
// Start HTTP server for web UI. Hub gives it read-only access to the
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# ============================================================
|
||||
# YAMA 网页端 nginx 反代示例(方案B:子路径部署)
|
||||
# 作为站点配置放入 /etc/nginx/sites-available/yama 后软链到 sites-enabled
|
||||
# 核心原则:nginx 只做 TLS 终止 + 完整路径转发,由 Go 端
|
||||
# YAMA_WEB_BASE_PATH=/remoter 负责 strip
|
||||
# ============================================================
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name 你的域名.com;
|
||||
|
||||
# TLS 证书(路径换成你自己的,如 certbot/letsencrypt 生成的位置)
|
||||
ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||
|
||||
# 裸 /remoter(没写结尾斜杠)→ 301 到 /remoter/
|
||||
location = /remoter {
|
||||
return 301 /remoter/;
|
||||
}
|
||||
|
||||
# 子路径:完整路径原样转发,Go 端按 YAMA_WEB_BASE_PATH 再 strip
|
||||
location /remoter/ {
|
||||
# 结尾没有 "/" → /remoter/... 原样转发(不要写成 http://127.0.0.1:8080/)
|
||||
# Go 服务端若在另一台机器,换成 http://192.168.1.50:8080;
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# WebSocket 升级(远程桌面走 /remoter/ws,不配这两行连不上)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 远程桌面连接长时间空闲,避免默认 60s 被 nginx 掐断
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
|
||||
# 大文件/流式传输时避免缓冲占内存(可选)
|
||||
# proxy_buffering off;
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 方案A(根路径部署):不设 YAMA_WEB_BASE_PATH,用下面这段替代上面
|
||||
# ============================================================
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name 你的域名.com;
|
||||
# ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8080;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_read_timeout 3600s;
|
||||
# proxy_send_timeout 3600s;
|
||||
# }
|
||||
# }
|
||||
@@ -0,0 +1,64 @@
|
||||
# ============================================================
|
||||
# YAMA 服务端环境变量(完整版)
|
||||
# systemd 通过 EnvironmentFile=-/etc/yama/server.env 读取
|
||||
# 行首 "#" = 未启用(保持默认);去掉 "#" 即生效
|
||||
# ============================================================
|
||||
|
||||
# ---------- 账号 / 密码 ----------
|
||||
|
||||
# Web 管理员账号 "admin" 的初始密码。回退链:
|
||||
# YAMA_WEB_ADMIN_PASS → YAMA_PWD → 硬编码 "admin"
|
||||
# 生产环境务必设置,否则会以 admin/admin 裸奔(启动日志会红色告警)
|
||||
YAMA_WEB_ADMIN_PASS=ChangeMe_Strong
|
||||
|
||||
# 超级密码(明文)。TCP 授权用,也是上面 Web 管理员密码的回退来源
|
||||
#YAMA_PWD=
|
||||
|
||||
# TCP 授权的密码哈希(SHA-256 十六进制)。与 YAMA_PWD 二选一;
|
||||
# 未设置时用内置占位默认值(生产应覆盖)
|
||||
#YAMA_PWDHASH=
|
||||
|
||||
# 持久化 Web 用户文件路径(默认相对工作目录的 users.json)
|
||||
#YAMA_USERS_FILE=/opt/yama/users.json
|
||||
|
||||
# ---------- 网页端 / 反向代理 ----------
|
||||
|
||||
# 子路径部署前缀(方案B)。空 = 根路径(方案A);有值须与 nginx location 一致
|
||||
YAMA_WEB_BASE_PATH=/remoter
|
||||
|
||||
# "1" = 信任反向代理的 X-Forwarded-For(nginx 在 Go 前面时务必开启,
|
||||
# 否则登录限流会把所有客户端当成 nginx 同一个 IP)
|
||||
YAMA_WEB_TRUST_PROXY=1
|
||||
|
||||
# WebSocket Origin 白名单(逗号分隔)。空 = 仅允许同源
|
||||
#YAMA_WEB_ALLOWED_ORIGINS=https://你的域名
|
||||
|
||||
# ---------- 授权 / 许可(signer)----------
|
||||
# 优先级:YAMA_SIGN_PASSWORD > YAMA_LICENSE_DISABLED=1 > YAMA_LICENSE_TOKEN > 匿名试用
|
||||
|
||||
# 本地签名主密钥(HMAC)。设置即进入 LocalSigner 模式(运营方自部署)
|
||||
#YAMA_SIGN_PASSWORD=
|
||||
|
||||
# "1" = 关闭授权校验(NoOpSigner,离线/开发用;客户端将拒绝屏幕/文件功能)
|
||||
#YAMA_LICENSE_DISABLED=1
|
||||
|
||||
# 许可服务器 URL(RemoteSigner 付费客户 / 试用模式的默认地址)
|
||||
#YAMA_LICENSE_SERVER=
|
||||
|
||||
# 客户 JWT(付费客户解锁设备配额)
|
||||
#YAMA_LICENSE_TOKEN=
|
||||
|
||||
# 缓存 TTL(小时,RemoteSigner 离线缓存,默认 24)
|
||||
#YAMA_LICENSE_OFFLINE_HRS=24
|
||||
|
||||
# ---------- 兼做许可服务器(可选,LocalSigner-as-LS)----------
|
||||
|
||||
# issue-token 子命令用:RSA 私钥 PEM 路径(与公钥配对)
|
||||
#YAMA_LICENSE_PRIVATE_KEY=
|
||||
|
||||
# 对外签发许可用:RSA 公钥 PEM 路径 + 监听地址(如 ":8443")
|
||||
#YAMA_LICENSE_PUBLIC_KEY=
|
||||
#YAMA_LICENSE_HTTP_ADDR=:8443
|
||||
|
||||
# 配额状态持久化文件路径
|
||||
#YAMA_LICENSE_STATE_PATH=/opt/yama/license_state.json
|
||||
@@ -0,0 +1,35 @@
|
||||
[Unit]
|
||||
Description=YAMA Remote Administration Server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=yama
|
||||
Group=yama
|
||||
WorkingDirectory=/opt/yama
|
||||
|
||||
# 密钥 / 部署参数集中放这里(权限 600)。前面的 "-" 表示文件不存在也不报错。
|
||||
EnvironmentFile=-/etc/yama/server.env
|
||||
|
||||
# 6543 = 客户端(agent)回连端口;8080 = 网页端 HTTP(由 nginx 反向代理)
|
||||
# arm64 机器请把二进制名换成 server_linux_arm64
|
||||
ExecStart=/opt/yama/server_linux_amd64 -port 6543 -http-port 8080 -no-console
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# --- 安全加固(可选,不需要可删掉整段) ---
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
# 若服务需要写 /opt/yama 之外的目录,再放开这一行:
|
||||
# ReadWritePaths=/opt/yama/data
|
||||
|
||||
# 日志交给 journald
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=yama
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+50
-7
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -30,6 +31,7 @@ type Server struct {
|
||||
loginIPLimit *wsauth.RateLimiter
|
||||
loginUserLimit *wsauth.RateLimiter
|
||||
trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only)
|
||||
basePath string // web UI mount prefix (e.g. "/remoter"); "" = domain root
|
||||
}
|
||||
|
||||
// Config tunes the server's exposed-on-public-HTTPS hardening knobs.
|
||||
@@ -52,6 +54,12 @@ type Config struct {
|
||||
// exposure the header is client-controlled and would let attackers
|
||||
// evade per-IP rate limits.
|
||||
TrustForwardedFor bool
|
||||
// BasePath mounts the whole web UI (index, manifest, static assets,
|
||||
// /health, /api/devices, /ws) under a URL prefix, so a reverse proxy
|
||||
// can serve it from a subpath of an existing domain (e.g.
|
||||
// https://example.com/remoter). Must be empty or a leading-slash,
|
||||
// no-trailing-slash path; "" (default) serves from the domain root.
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// New creates an HTTP server bound to the given port. port=0 disables the server.
|
||||
@@ -68,6 +76,7 @@ func (s *Server) WithConfig(cfg Config) *Server {
|
||||
s.loginIPLimit = cfg.LoginIPLimit
|
||||
s.loginUserLimit = cfg.LoginUserLimit
|
||||
s.trustForwardedFor = cfg.TrustForwardedFor
|
||||
s.basePath = cfg.BasePath
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -96,7 +105,7 @@ func (s *Server) Start() error {
|
||||
|
||||
s.srv = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(s.port),
|
||||
Handler: mux,
|
||||
Handler: s.mount(mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
@@ -129,14 +138,41 @@ func (s *Server) Stop() {
|
||||
_ = s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// basePathPlaceholder is the token in index.html substituted with the
|
||||
// configured base path at serve time. Empty base path → "" → root URLs.
|
||||
const basePathPlaceholder = "__BASE_PATH__"
|
||||
|
||||
// mount wraps the root-path mux with the configured base path. When basePath
|
||||
// is empty the mux is returned unchanged (domain-root serving, 方案A). Otherwise
|
||||
// the bare prefix redirects to prefix+"/", paths outside the prefix 404, and
|
||||
// matching paths are stripped before reaching the mux (方案B, subpath serving).
|
||||
func (s *Server) mount(h http.Handler) http.Handler {
|
||||
if s.basePath == "" {
|
||||
return h
|
||||
}
|
||||
base := s.basePath // normalized by cmd: leading "/", no trailing "/"
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == base {
|
||||
http.Redirect(w, r, base+"/", http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, base+"/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.StripPrefix(base, h).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
body := bytes.ReplaceAll(IndexHTML, []byte(basePathPlaceholder), []byte(s.basePath))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_, _ = w.Write(IndexHTML)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -190,21 +226,28 @@ func (s *Server) unauthorized(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
|
||||
// Static JSON, no template needed.
|
||||
const manifestJSON = `{
|
||||
// start_url is rooted at the configured base path so an installed PWA
|
||||
// launches into the right location; "/" when served from the domain root.
|
||||
func (s *Server) manifestJSON() []byte {
|
||||
start := "/"
|
||||
if s.basePath != "" {
|
||||
start = s.basePath + "/"
|
||||
}
|
||||
return []byte(fmt.Sprintf(`{
|
||||
"name": "SimpleRemoter",
|
||||
"short_name": "Remoter",
|
||||
"start_url": "/",
|
||||
"start_url": "%s",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#1a1a2e",
|
||||
"theme_color": "#1a1a2e"
|
||||
}`
|
||||
}`, start))
|
||||
}
|
||||
|
||||
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
_, _ = w.Write([]byte(manifestJSON))
|
||||
_, _ = w.Write(s.manifestJSON())
|
||||
}
|
||||
|
||||
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Remoter">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="manifest" href="__BASE_PATH__/manifest.json">
|
||||
<!-- xterm.js (远程终端). Files served by WebService from RC binary resources. -->
|
||||
<link rel="stylesheet" href="/static/xterm.css">
|
||||
<link rel="stylesheet" href="__BASE_PATH__/static/xterm.css">
|
||||
<link rel="icon" type="image/png" href="data:image/png;base64,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">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
@@ -1349,8 +1349,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/xterm.js"></script>
|
||||
<script src="/static/xterm-fit.js"></script>
|
||||
<script src="__BASE_PATH__/static/xterm.js"></script>
|
||||
<script src="__BASE_PATH__/static/xterm-fit.js"></script>
|
||||
|
||||
<!-- Opus codec for audio decompression -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/opus.js@0.5.0/dist/opus.js"></script>
|
||||
@@ -1524,7 +1524,7 @@
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
updateWsStatus('connecting');
|
||||
try {
|
||||
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
||||
ws = new WebSocket(protocol + '//' + location.host + '__BASE_PATH__/ws');
|
||||
} catch (e) {
|
||||
updateWsStatus('disconnected');
|
||||
scheduleReconnect();
|
||||
|
||||
Reference in new issue
Block a user