From 2f6bb26e978933e2324a36fd8cc39d2ad25c70c4 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 23 Jul 2026 10:55:17 +0300 Subject: [PATCH] Normalize base address in Node class to ensure consistent URI formatting - Implemented `NormalizeBaseAddress` method to add a trailing slash to the `BaseAddress` when necessary, preventing issues with relative path merging in 3x-ui deployments. - Updated `Register` and `UpdateAddress` methods to utilize the normalization logic, ensuring that both methods handle base address input consistently. - Added unit tests to verify the normalization behavior for various input scenarios, enhancing the reliability of node address management. - Updated domain model documentation to reflect the new normalization behavior for `BaseAddress`. --- backend/src/PnvPanel.Domain/Nodes/Node.cs | 13 +++++++++-- .../PnvPanel.Domain.Tests/Nodes/NodeTests.cs | 23 +++++++++++++++++++ docs/domain-model.md | 7 ++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/backend/src/PnvPanel.Domain/Nodes/Node.cs b/backend/src/PnvPanel.Domain/Nodes/Node.cs index 02842b6..0555a86 100644 --- a/backend/src/PnvPanel.Domain/Nodes/Node.cs +++ b/backend/src/PnvPanel.Domain/Nodes/Node.cs @@ -34,7 +34,7 @@ public sealed class Node : Entity { Id = Guid.NewGuid(), Name = name, - BaseAddress = baseAddress, + BaseAddress = NormalizeBaseAddress(baseAddress), Credentials = credentials, Location = location, Status = NodeStatus.Unknown, @@ -54,9 +54,18 @@ public sealed class Node : Entity if (!baseAddress.IsAbsoluteUri) throw new DomainException("Адрес ноды должен быть абсолютным URI."); - BaseAddress = baseAddress; + BaseAddress = NormalizeBaseAddress(baseAddress); } + /// 3x-ui часто разворачивается за нестандартным base path (`webBasePath`, напр. + /// `/benis`) — без завершающего слэша относительное объединение пути (RFC 3986 merge) отбрасывает + /// последний сегмент базы вместо добавления к нему, роняя `/benis` из итогового URL. Нормализуем + /// один раз при сохранении, чтобы админ мог вводить адрес и с завершающим слэшем, и без него. + private static Uri NormalizeBaseAddress(Uri baseAddress) => + baseAddress.AbsolutePath.EndsWith('/') + ? baseAddress + : new UriBuilder(baseAddress) { Path = baseAddress.AbsolutePath + "/" }.Uri; + public void UpdateCredentials(NodeCredentials credentials) => Credentials = credentials; public void Enable() => IsEnabled = true; diff --git a/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs b/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs index d7a2eab..4c644fc 100644 --- a/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs +++ b/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs @@ -35,6 +35,29 @@ public class NodeTests ); } + [Theory] + [InlineData("https://host.example.com/benis", "https://host.example.com/benis/")] + [InlineData("https://host.example.com/benis/", "https://host.example.com/benis/")] + [InlineData("https://host.example.com", "https://host.example.com/")] + public void Register_NormalizesBaseAddressToTrailingSlash(string input, string expected) + { + var node = Node.Register("Node", new Uri(input), Credentials, null); + + Assert.Equal(expected, node.BaseAddress.ToString()); + } + + [Theory] + [InlineData("https://host.example.com/benis", "https://host.example.com/benis/")] + [InlineData("https://host.example.com/benis/", "https://host.example.com/benis/")] + public void UpdateAddress_NormalizesBaseAddressToTrailingSlash(string input, string expected) + { + var node = Node.Register("Node", new Uri("https://old.example.com"), Credentials, null); + + node.UpdateAddress(new Uri(input)); + + Assert.Equal(expected, node.BaseAddress.ToString()); + } + [Fact] public void UpdateDetails_ChangesNameAndLocation() { diff --git a/docs/domain-model.md b/docs/domain-model.md index 103049e..b5ada7f 100644 --- a/docs/domain-model.md +++ b/docs/domain-model.md @@ -55,6 +55,13 @@ AppUser конфиги на ноде запрещены, но **существующие не трогаем** (клиенты остаются в 3x-ui). Статус ноды показываем пользователю как индикатор «состояние сервера». +`BaseAddress` **нормализуется к завершающему `/`** (`Register`/`UpdateAddress`) — 3x-ui часто стоит за +нестандартным base path (`webBasePath`, напр. `https://host/benis`, панель тогда доступна по +`/benis/panel/...`); без завершающего слэша относительное объединение пути (RFC 3986 merge) отбрасывает +последний сегмент базы вместо добавления к нему, роняя `/benis` из итогового URL при запросах к панели. +Нормализация — единственная защита от этого на уровне PnvPanel; админ может вводить адрес и со слэшем, +и без него — результат одинаковый. + ### Inbound — прокси-inbound на ноде Проекция inbound из 3x-ui; определяет протокол и параметры подключения.