From a06ed5806a2a6aae77de39811ce50eea5ca6e4d4 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 9 Oct 2026 15:34:12 +0800 Subject: [PATCH] [fix] support self-hosted server URLs with paths --- .../src/config_center/config_center.cpp | 22 +++++- .../desktop/src/config_center/config_center.h | 1 + .../src/gui/application/gui_application.cpp | 73 +++++++++++-------- .../src/gui/application/gui_application.h | 3 +- .../assets/localization/localization_data.h | 3 + .../src/gui/runtime/announcement_inbox.cpp | 9 ++- apps/desktop/src/gui/ui/main_window.slint | 23 ++++-- .../linux/headless/headless_settings.cpp | 5 +- apps/desktop/xmake/targets.lua | 1 + deps/submodules/minirtc | 2 +- docs/HEADLESS.md | 2 +- docs/HEADLESS_EN.md | 2 +- docs/SELF_HOSTING.md | 4 +- docs/SELF_HOSTING_EN.md | 2 +- 14 files changed, 101 insertions(+), 51 deletions(-) diff --git a/apps/desktop/src/config_center/config_center.cpp b/apps/desktop/src/config_center/config_center.cpp index 5ce94917..caa636bb 100644 --- a/apps/desktop/src/config_center/config_center.cpp +++ b/apps/desktop/src/config_center/config_center.cpp @@ -10,6 +10,7 @@ #include "SimpleIni.h" #include "autostart.h" #include "rd_log.h" +#include "signal_server_address.h" namespace crossdesk { @@ -290,10 +291,23 @@ int ConfigCenter::SetTurn(bool enable_turn, bool force_relay) { } int ConfigCenter::SetServerHost(const std::string& host) { - if (host.empty() || host.size() > 253 || host.find_first_of("/\\@?#") != std::string::npos || - std::any_of(host.begin(), host.end(), [](unsigned char c) { return c <= 32 || c == 127; })) return -1; - if (StoreValues({{"signal_server_host", host}}) != 0) return -1; - signal_server_host_ = host; + const auto address = minirtc::ParseSignalServerAddress(host); + if (!address) return -1; + if (address->port) return SetServerEndpoint(host, *address->port); + if (StoreValues({{"signal_server_host", address->Address()}}) != 0) return -1; + signal_server_host_ = address->Address(); + return 0; +} + +int ConfigCenter::SetServerEndpoint(const std::string& host, int port) { + const auto address = minirtc::ParseSignalServerAddress(host); + if (!address) return -1; + port = address->port.value_or(port); + if (port < 1 || port > 65535) return -1; + if (StoreValues({{"signal_server_host", address->Address()}, + {"signal_server_port", std::to_string(port)}}) != 0) return -1; + signal_server_host_ = address->Address(); + signal_server_port_ = port; return 0; } diff --git a/apps/desktop/src/config_center/config_center.h b/apps/desktop/src/config_center/config_center.h index 6ed294be..3b4fc82b 100644 --- a/apps/desktop/src/config_center/config_center.h +++ b/apps/desktop/src/config_center/config_center.h @@ -42,6 +42,7 @@ class ConfigCenter { int SetTurn(bool enable_turn, bool force_relay); int SetServerHost(const std::string& signal_server_host); int SetServerPort(int signal_server_port); + int SetServerEndpoint(const std::string& address, int port); int SetSelfHosted(bool enable_self_hosted); int SetAutostart(bool enable_autostart); int SetDaemon(bool enable_daemon); diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 89f424d2..07494d23 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -63,6 +63,7 @@ #include "rd_log.h" #include "server_window_state.h" #include "signal_server_settings.h" +#include "signal_server_address.h" #include "ui/announcement_text.h" #include "ui/ui_localization.h" #include "update_checker.h" @@ -1285,6 +1286,7 @@ void GuiApplication::ResetSettingsUi() { ui_->main->set_file_save_path(file_transfer_save_path_buf_); ui_->main->set_server_host(signal_server_ip_self_); ui_->main->set_server_port(signal_server_port_self_); + ui_->main->set_self_host_settings_error(""); } void GuiApplication::BindMainCallbacks() { @@ -1419,29 +1421,14 @@ void GuiApplication::BindMainCallbacks() { ui_->main->set_file_save_path(folder); } }); - main->on_save_settings([this] { SaveSettingsFromUi(); }); + main->on_save_settings([this] { return SaveSettingsFromUi(); }); main->on_cancel_settings([this] { ResetSettingsUi(); }); main->on_save_self_hosted_settings([this] { - const std::string host(ui_->main->get_server_host()); - const auto signal_port = ParsePort(ui_->main->get_server_port()); - if (!host.empty()) { - config_center_->SetServerHost(host); - std::memset(signal_server_ip_self_, 0, sizeof(signal_server_ip_self_)); - std::strncpy(signal_server_ip_self_, host.c_str(), - sizeof(signal_server_ip_self_) - 1); - std::memset(signal_server_ip_, 0, sizeof(signal_server_ip_)); - std::strncpy(signal_server_ip_, host.c_str(), - sizeof(signal_server_ip_) - 1); - } - if (signal_port) { - config_center_->SetServerPort(*signal_port); - std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_), - "%d", *signal_port); - std::snprintf(signal_server_port_, sizeof(signal_server_port_), "%d", - *signal_port); - } + // Stage the endpoint until the surrounding settings dialog is confirmed. + return ValidateSelfHostedSettingsFromUi(); }); main->on_cancel_self_hosted_settings([this] { + ui_->main->set_self_host_settings_error(""); const std::string host = config_center_->GetSignalServerHost(); ui_->main->set_server_host(UiText(host)); const int signal_port = config_center_->GetSignalServerPort(); @@ -3496,9 +3483,45 @@ void GuiApplication::SyncServerWindow() { PositionServerWindows(); } -void GuiApplication::SaveSettingsFromUi() { +bool GuiApplication::ValidateSelfHostedSettingsFromUi() { + auto& main = ui_->main; + const auto address = minirtc::ParseSignalServerAddress( + std::string(main->get_server_host())); + if (!address) { + main->set_self_host_settings_error(UiText( + localization::self_hosted_address_invalid[localization_language_index_])); + return false; + } + const auto port = address->port ? address->port : ParsePort(main->get_server_port()); + if (!port) { + main->set_self_host_settings_error(UiText( + localization::self_hosted_port_invalid[localization_language_index_])); + return false; + } + main->set_server_host(UiText(address->Address())); + main->set_server_port(UiText(std::to_string(*port))); + main->set_self_host_settings_error(""); + return true; +} + +bool GuiApplication::SaveSettingsFromUi() { const auto previous_server = GetSignalServerSettings(*config_center_); auto& main = ui_->main; + const bool has_endpoint = main->get_self_hosted_enabled() || + !std::string(main->get_server_host()).empty() || + !std::string(main->get_server_port()).empty(); + if (has_endpoint && !ValidateSelfHostedSettingsFromUi()) { + main->set_self_host_settings_open(true); + return false; + } + if ((has_endpoint && config_center_->SetServerEndpoint( + std::string(main->get_server_host()), *ParsePort(main->get_server_port())) != 0) || + config_center_->SetSelfHosted(main->get_self_hosted_enabled()) != 0) { + main->set_self_host_settings_error(UiText( + localization::self_hosted_save_failed[localization_language_index_])); + main->set_self_host_settings_open(true); + return false; + } language_button_value_ = localization::detail::ClampLanguageIndex(main->get_language_index()); video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1); @@ -3564,17 +3587,9 @@ void GuiApplication::SaveSettingsFromUi() { sizeof(file_transfer_save_path_buf_) - 1); config_center_->SetFileTransferSavePath(path); - const std::string host(main->get_server_host()); - if (!host.empty()) { - config_center_->SetServerHost(host); - } - if (const auto port = ParsePort(main->get_server_port())) { - config_center_->SetServerPort(*port); - } - - config_center_->SetSelfHosted(enable_self_hosted_); ApplySettingsFromConfig(previous_server != GetSignalServerSettings(*config_center_)); + return true; } void GuiApplication::ApplySettingsFromConfig(bool reconnect) { diff --git a/apps/desktop/src/gui/application/gui_application.h b/apps/desktop/src/gui/application/gui_application.h index 47f66f6c..4a840c1d 100644 --- a/apps/desktop/src/gui/application/gui_application.h +++ b/apps/desktop/src/gui/application/gui_application.h @@ -76,7 +76,8 @@ private: #endif void UpdateLocalization(); void ResetSettingsUi(); - void SaveSettingsFromUi(); + bool ValidateSelfHostedSettingsFromUi(); + bool SaveSettingsFromUi(); void ApplySettingsFromConfig(bool reconnect); #if _WIN32 && CROSSDESK_PORTABLE void CheckPortableWindowsService(); diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index aad6f00d..b8e36db3 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -240,6 +240,9 @@ struct TranslationRow { "Server Address:", u8"Адрес сервера:") \ X(self_hosted_server_port, u8"信令服务端口:", \ "Signal Service Port:", u8"Порт сигнального сервиса:") \ + X(self_hosted_address_invalid, u8"请输入有效的主机名、IP 或 HTTPS/WSS 信令地址,可包含路径。", "Enter a valid host, IP, or HTTPS/WSS signaling URL. Paths are supported.", u8"Введите имя хоста, IP или HTTPS/WSS-адрес сигнального сервиса. Путь допустим.") \ + X(self_hosted_port_invalid, u8"请输入 1–65535 范围内的信令端口。", "Enter a signaling port between 1 and 65535.", u8"Введите порт сигнального сервиса от 1 до 65535.") \ + X(self_hosted_save_failed, u8"配置保存失败,请检查配置目录是否可写后重试。", "Could not save settings. Check that the configuration directory is writable and retry.", u8"Не удалось сохранить настройки. Проверьте доступ на запись в каталог конфигурации.") \ X(ok, u8"确认", "OK", u8"ОК") \ X(cancel, u8"取消", "Cancel", u8"Отмена") \ X(new_password, u8"请输入六位密码:", \ diff --git a/apps/desktop/src/gui/runtime/announcement_inbox.cpp b/apps/desktop/src/gui/runtime/announcement_inbox.cpp index c2b1d27f..721246e3 100644 --- a/apps/desktop/src/gui/runtime/announcement_inbox.cpp +++ b/apps/desktop/src/gui/runtime/announcement_inbox.cpp @@ -8,6 +8,8 @@ #include #include +#include "signal_server_address.h" + #ifdef _WIN32 #include #endif @@ -51,9 +53,12 @@ void AnnouncementInbox::Configure(std::string host, int port, std::lock_guard lock(mutex_); if (directory_.empty() || host.empty() || port <= 0 || device_id.empty()) return; - for (auto& ch : host) + auto address = minirtc::ParseSignalServerAddress(host); + if (!address) return; + for (auto& ch : address->host) if (ch >= 'A' && ch <= 'Z') ch += 'a' - 'A'; - if (host.back() == '.') host.pop_back(); + if (address->host.back() == '.') address->host.pop_back(); + host = address->Address(); const auto scope = json::array({host, port, device_id}).dump(); if (scope_ == scope) return; scope_ = scope; diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index a2f5721e..ae6db768 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -379,6 +379,7 @@ export component MainWindow inherits Window { in-out property file-save-path: ""; in-out property server-host: ""; in-out property server-port: ""; + in-out property self-host-settings-error: ""; in property settings-session-active: false; // Keep the setting visible on every platform. Unsupported builds expose it // as disabled so the fixed settings layout never turns into a blank row. @@ -393,9 +394,9 @@ export component MainWindow inherits Window { callback recent-connect(string); callback recent-edit-alias(string, string); callback recent-delete(string); - callback save-settings; + callback save-settings() -> bool; callback cancel-settings; - callback save-self-hosted-settings; + callback save-self-hosted-settings() -> bool; callback cancel-self-hosted-settings; callback browse-save-path; callback open-download; @@ -418,6 +419,9 @@ export component MainWindow inherits Window { in-out property settings-open: false; private property settings-scroll-y: 0px; in-out property self-host-settings-open: false; + changed self-host-settings-open => { + if !root.self-host-settings-open { root.self-host-settings-error = ""; } + } in-out property about-open: false; in-out property update-open: false; in-out property reset-password-open: false; @@ -1809,7 +1813,7 @@ export component MainWindow inherits Window { } Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; } - CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } } + CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } } } CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } } } } @@ -1822,7 +1826,7 @@ export component MainWindow inherits Window { x: root.compact-language ? 190px : 140px; y: 105px; width: root.compact-language ? 260px : 360px; - height: 138px; + height: root.self-host-settings-error == "" ? 138px : 202px; border-radius: 7px; Rectangle { @@ -1846,7 +1850,7 @@ export component MainWindow inherits Window { border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 4px; - LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; } + LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; input-type: InputType.text; font-size: ImGuiFontStyle.body; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } } Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } @@ -1862,8 +1866,13 @@ export component MainWindow inherits Window { LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; } LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; } } - CompactButton { x: root.compact-language ? 93px : 135px; y: 100px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } } - CompactButton { x: root.compact-language ? 134px : 176px; y: 100px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } } + if root.self-host-settings-error != "": Text { + x: 8px; y: 89px; width: parent.width - 16px; height: 64px; + text: root.self-host-settings-error; color: #b3261e; + font-size: ImGuiFontStyle.body; wrap: word-wrap; + } + CompactButton { x: root.compact-language ? 93px : 135px; y: parent.height - 38px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } } + CompactButton { x: root.compact-language ? 134px : 176px; y: parent.height - 38px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } } } } diff --git a/apps/desktop/src/platform/linux/headless/headless_settings.cpp b/apps/desktop/src/platform/linux/headless/headless_settings.cpp index 6ba3761d..d63f3723 100644 --- a/apps/desktop/src/platform/linux/headless/headless_settings.cpp +++ b/apps/desktop/src/platform/linux/headless/headless_settings.cpp @@ -10,6 +10,7 @@ #include #include "config_center.h" +#include "signal_server_address.h" #include "signal_server_settings.h" namespace crossdesk { @@ -83,9 +84,7 @@ SettingResult ApplyConsoleSetting(ConfigCenter& config, const std::string& key, canonical = value; save = [&config, mode] { return config.SetTurnMode(static_cast(mode)); }; } else if (key == "server_host") { - if (input.empty() || input.size() > 253 || - input.find_first_of("/\\@?#") != std::string::npos || - std::any_of(input.begin(), input.end(), [](unsigned char c) { return c <= 32 || c == 127; })) return result; + if (!minirtc::ParseSignalServerAddress(input)) return result; save = [&] { return config.SetServerHost(input); }; } else if (key == "server_port") { int port = 0; diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index b06c524d..985a71a3 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -453,6 +453,7 @@ function setup_targets() add_deps("rd_log", "autostart") add_files("apps/desktop/src/config_center/*.cpp") add_includedirs("apps/desktop/src", "apps/desktop/src/config_center", + "deps/submodules/minirtc/src/api", {public = true}) target("assets") diff --git a/deps/submodules/minirtc b/deps/submodules/minirtc index 3f6f7f27..d2f42cb0 160000 --- a/deps/submodules/minirtc +++ b/deps/submodules/minirtc @@ -1 +1 @@ -Subproject commit 3f6f7f2725a8bc1e5b4350cd8e6a6e534d7fea73 +Subproject commit d2f42cb02c9f9eef471ce32f349dbad61cf6f34b diff --git a/docs/HEADLESS.md b/docs/HEADLESS.md index 8f6c36e3..42652b00 100644 --- a/docs/HEADLESS.md +++ b/docs/HEADLESS.md @@ -61,7 +61,7 @@ xmake r crossdesk --headless | `codec` | `h264` / `av1` | 保存后重新连接 | | `hardware` | `on` / `off` | 保存后重新连接;构建不支持时拒绝开启 | | `turn` | `off` / `auto` / `udp` / `tcp` | 保存后重新连接;`udp` / `tcp` 表示强制中继 | -| `server_host` | 主机名或 IP,不含协议与路径 | 自托管未启用时保存备用,已启用时重连 | +| `server_host` | 主机名、IP 或 HTTPS/WSS 信令地址,支持路径和显式端口 | 自托管未启用时保存备用,已启用时重连 | | `server_port` | `1`–`65535` | 同上 | | `self_hosted` | `on` / `off` | 先配置地址、端口,再开启;保存后重连 | | `privacy` | `on` / `off` | 后续远程连接的自动隐私屏行为 | diff --git a/docs/HEADLESS_EN.md b/docs/HEADLESS_EN.md index a0d32043..d33202bd 100644 --- a/docs/HEADLESS_EN.md +++ b/docs/HEADLESS_EN.md @@ -60,7 +60,7 @@ The menu shares the GUI's `config.ini`. Each valid change is saved immediately; | `codec` | `h264` / `av1` | Reconnect after saving | | `hardware` | `on` / `off` | Reconnect; enabling requires a build with hardware codec support | | `turn` | `off` / `auto` / `udp` / `tcp` | Reconnect; UDP/TCP force relay use | -| `server_host` | Hostname or IP without URL scheme/path | Reconnect if self-hosting is enabled; otherwise saved for later | +| `server_host` | Hostname, IP, or HTTPS/WSS signaling URL, with optional path and explicit port | Reconnect if self-hosting is enabled; otherwise saved for later | | `server_port` | `1`–`65535` | Same as server host | | `self_hosted` | `on` / `off` | Configure host/port first, then enable; reconnect | | `privacy` | `on` / `off` | Automatic privacy screen on subsequent connections | diff --git a/docs/SELF_HOSTING.md b/docs/SELF_HOSTING.md index 6ac0dae0..0d0bb746 100644 --- a/docs/SELF_HOSTING.md +++ b/docs/SELF_HOSTING.md @@ -44,10 +44,12 @@ sudo docker compose ps 1. 断开现有会话,点击主窗口右上角 **☰ → 设置**。 2. 在设置列表中向下滚动,点击 **自托管配置**。 -3. 填写 **服务器地址**、**信令服务端口**,点击“确认”。地址只填主机名或 IP,不加 `https://`、路径或端口。 +3. 填写 **服务器地址**、**信令服务端口**,点击“确认”。地址支持主机名或 IP(如 `my-server.example.com`),也支持带路径的信令地址(如 `my-server.example.com/signal-path`)或 `https://` / `wss://` URL。URL 内明确填写的端口优先;否则使用单独填写的信令服务端口。所有连接均使用 WSS,带路径的地址需要反向代理将该路径转发到信令 WebSocket 服务,普通网页地址不能直接用于信令连接。 4. 回到设置窗口,勾选 **自托管配置** 右侧复选框,再点击“确认”保存。 5. 在控制端和被控端执行相同配置。等待两端都显示“已连接服务器”,再使用被控端当前显示的 ID 发起连接。 +地址与端口在最后确认设置时一起保存;格式无效或保存失败时会保留输入并显示错误。 +

当前 Slint 客户端自托管设置:服务器地址与信令端口

diff --git a/docs/SELF_HOSTING_EN.md b/docs/SELF_HOSTING_EN.md index 122dbcd9..8d86f35c 100644 --- a/docs/SELF_HOSTING_EN.md +++ b/docs/SELF_HOSTING_EN.md @@ -44,7 +44,7 @@ For a self-signed deployment, complete the [certificate trust steps](#3-trust-th 1. Disconnect existing sessions and open **☰ → Settings**. 2. Scroll down and click **Self-Hosted Config**. -3. Enter **Server Address** and **Signal Service Port**, then confirm. Enter only the hostname or IP in the address field, without a URL scheme, path, or port. +3. Enter **Server Address** and **Signal Service Port**, then confirm. The address accepts a hostname or IP (such as `my-server.example.com`), a signaling path (such as `my-server.example.com/signal-path`), or an `https://` / `wss://` URL. An explicit URL port takes precedence; otherwise the separate signaling port is used. Connections always use WSS. Paths must be routed to the signaling WebSocket service by your reverse proxy; an ordinary web page is not a signaling endpoint. 4. Enable the checkbox beside **Self-Hosted Config**, then click **OK** in the parent settings window. 5. Configure both the controller and host this way. Once both report a server connection, connect using the host's currently displayed ID.