diff --git a/src/ByteSync.Client/Assets/Resources/Resources.Designer.cs b/src/ByteSync.Client/Assets/Resources/Resources.Designer.cs index 04b90231..376f683f 100644 --- a/src/ByteSync.Client/Assets/Resources/Resources.Designer.cs +++ b/src/ByteSync.Client/Assets/Resources/Resources.Designer.cs @@ -2304,6 +2304,18 @@ public static string DataSourceChecker_SubPathError_Message { } } + public static string DataSourceChecker_ProtectedPathError_Title { + get { + return ResourceManager.GetString("DataSourceChecker_ProtectedPathError_Title", resourceCulture); + } + } + + public static string DataSourceChecker_ProtectedPathError_Message { + get { + return ResourceManager.GetString("DataSourceChecker_ProtectedPathError_Message", resourceCulture); + } + } + public static string SessionQuitChecker_CanNotRestart_Title { get { return ResourceManager.GetString("SessionQuitChecker_CanNotRestart_Title", resourceCulture); diff --git a/src/ByteSync.Client/Assets/Resources/Resources.fr.resx b/src/ByteSync.Client/Assets/Resources/Resources.fr.resx index 18a47968..1f53c5f1 100644 --- a/src/ByteSync.Client/Assets/Resources/Resources.fr.resx +++ b/src/ByteSync.Client/Assets/Resources/Resources.fr.resx @@ -897,6 +897,12 @@ Vous devez sélectionner au minimum 2 Nœuds de Données. Il n'est pas possible d'ajouter cet élément pour la raison suivante : Soit cet élément, soit un parent, soit un descendant est déjà présent. + + Impossible d'ajouter la source + + + Le chemin sélectionné "{0}" correspond à un emplacement système protégé et ne peut pas être inventorié ni synchronisé. + Fichiers et répertoires diff --git a/src/ByteSync.Client/Assets/Resources/Resources.resx b/src/ByteSync.Client/Assets/Resources/Resources.resx index b4f0199e..395492ca 100644 --- a/src/ByteSync.Client/Assets/Resources/Resources.resx +++ b/src/ByteSync.Client/Assets/Resources/Resources.resx @@ -921,6 +921,12 @@ You must select at least 2 Data Nodes. It is not possible to add this element for the following reason: Either this element, or a parent, or a descendant is already present. + + Unable to add the data source + + + The selected path "{0}" points to a protected system location and cannot be inventoried or synchronized. + Files and directories diff --git a/src/ByteSync.Client/Factories/InventoryBuilderFactory.cs b/src/ByteSync.Client/Factories/InventoryBuilderFactory.cs index 7144a913..daa517ac 100644 --- a/src/ByteSync.Client/Factories/InventoryBuilderFactory.cs +++ b/src/ByteSync.Client/Factories/InventoryBuilderFactory.cs @@ -10,16 +10,19 @@ using ByteSync.Interfaces.Factories; using ByteSync.Interfaces.Repositories; using ByteSync.Interfaces.Services.Sessions; +using Microsoft.Extensions.Logging; namespace ByteSync.Factories; public class InventoryBuilderFactory : IInventoryBuilderFactory { private readonly IComponentContext _context; + private readonly ILogger _logger; - public InventoryBuilderFactory(IComponentContext context) + public InventoryBuilderFactory(IComponentContext context, ILogger logger) { _context = context; + _logger = logger; } public IInventoryBuilder CreateInventoryBuilder(DataNode dataNode) @@ -54,9 +57,18 @@ public IInventoryBuilder CreateInventoryBuilder(DataNode dataNode) foreach (var dataSource in myDataSources) { - inventoryBuilder.AddInventoryPart(dataSource); + try + { + inventoryBuilder.AddInventoryPart(dataSource); + } + catch (Exception ex) + { + _logger.LogWarning(ex, + "InventoryBuilderFactory: Failed to add data source {Path} for DataNode {DataNodeId}", + dataSource.Path, dataNode.Id); + } } return inventoryBuilder; } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Helpers/ProtectedPaths.cs b/src/ByteSync.Client/Helpers/ProtectedPaths.cs new file mode 100644 index 00000000..0cbfe427 --- /dev/null +++ b/src/ByteSync.Client/Helpers/ProtectedPaths.cs @@ -0,0 +1,67 @@ +using System.IO; +using ByteSync.Common.Business.Misc; + +namespace ByteSync.Helpers; + +public static class ProtectedPaths +{ + private static readonly string[] _protectedRoots = + [ + "/dev", + "/proc", + "/sys", + "/run", + "/var/run", + "/private/var/run" + ]; + + public static bool TryGetProtectedRoot(string path, OSPlatforms osPlatform, out string protectedRoot) + { + protectedRoot = string.Empty; + + if (osPlatform == OSPlatforms.Windows) + { + return false; + } + + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + var normalizedPath = Normalize(path); + + foreach (var root in _protectedRoots) + { + var normalizedRoot = Normalize(root); + + if (IsSameOrSubPath(normalizedPath, normalizedRoot, StringComparison.Ordinal)) + { + protectedRoot = root; + + return true; + } + } + + return false; + } + + private static string Normalize(string path) + { + var fullPath = Path.GetFullPath(path); + + return Path.TrimEndingDirectorySeparator(fullPath); + } + + private static bool IsSameOrSubPath(string path, string root, StringComparison comparison) + { + if (path.Equals(root, comparison)) + { + return true; + } + + var rootWithSeparator = root + Path.DirectorySeparatorChar; + + return path.StartsWith(rootWithSeparator, comparison); + } +} \ No newline at end of file diff --git a/src/ByteSync.Client/Services/Inventories/DataSourceChecker.cs b/src/ByteSync.Client/Services/Inventories/DataSourceChecker.cs index 2465951e..3a4fb9c8 100644 --- a/src/ByteSync.Client/Services/Inventories/DataSourceChecker.cs +++ b/src/ByteSync.Client/Services/Inventories/DataSourceChecker.cs @@ -1,7 +1,9 @@ -using ByteSync.Assets.Resources; +using ByteSync.Assets.Resources; using ByteSync.Business.DataSources; using ByteSync.Common.Business.Inventories; +using ByteSync.Helpers; using ByteSync.Interfaces; +using ByteSync.Interfaces.Controls.Applications; using ByteSync.Interfaces.Dialogs; namespace ByteSync.Services.Inventories; @@ -9,41 +11,56 @@ namespace ByteSync.Services.Inventories; public class DataSourceChecker : IDataSourceChecker { private readonly IDialogService _dialogService; + private readonly IEnvironmentService _environmentService; + private readonly ILogger _logger; - public DataSourceChecker(IDialogService dialogService) + public DataSourceChecker(IDialogService dialogService, IEnvironmentService environmentService, ILogger logger) { _dialogService = dialogService; + _environmentService = environmentService; + _logger = logger; } public async Task CheckDataSource(DataSource dataSource, IEnumerable existingDataSources) { + if (dataSource.ClientInstanceId == _environmentService.ClientInstanceId + && ProtectedPaths.TryGetProtectedRoot(dataSource.Path, _environmentService.OSPlatform, out var protectedRoot)) + { + _logger.LogWarning("Blocked data source path {Path} because it is under protected root {ProtectedRoot}", + dataSource.Path, protectedRoot); + await ShowProtectedPathError(dataSource.Path); + + return false; + } + if (dataSource.Type == FileSystemTypes.File) { if (existingDataSources.Any(ds => ds.ClientInstanceId.Equals(dataSource.ClientInstanceId) && ds.Type == FileSystemTypes.File - && ds.Path.Equals(dataSource.Path, StringComparison.InvariantCultureIgnoreCase))) + && ds.Path.Equals(dataSource.Path, StringComparison.InvariantCultureIgnoreCase))) { await ShowError(); - + return false; } } else { // We can neither be equal, nor be, nor be a parent of an already selected path - if (existingDataSources.Any(ds => ds.ClientInstanceId.Equals(dataSource.ClientInstanceId) && ds.Type == FileSystemTypes.Directory - && (ds.Path.Equals(dataSource.Path, StringComparison.InvariantCultureIgnoreCase) || - IOUtils.IsSubPathOf(ds.Path, dataSource.Path) || - IOUtils.IsSubPathOf(dataSource.Path, ds.Path)))) + if (existingDataSources.Any(ds => ds.ClientInstanceId.Equals(dataSource.ClientInstanceId) && + ds.Type == FileSystemTypes.Directory + && (ds.Path.Equals(dataSource.Path, StringComparison.InvariantCultureIgnoreCase) || + IOUtils.IsSubPathOf(ds.Path, dataSource.Path) || + IOUtils.IsSubPathOf(dataSource.Path, ds.Path)))) { await ShowError(); - + return false; } } - + return true; } - + private async Task ShowError() { var messageBoxViewModel = _dialogService.CreateMessageBoxViewModel( @@ -51,4 +68,14 @@ private async Task ShowError() messageBoxViewModel.ShowOK = true; await _dialogService.ShowMessageBoxAsync(messageBoxViewModel); } + + private async Task ShowProtectedPathError(string path) + { + var messageBoxViewModel = _dialogService.CreateMessageBoxViewModel( + nameof(Resources.DataSourceChecker_ProtectedPathError_Title), + nameof(Resources.DataSourceChecker_ProtectedPathError_Message), + path); + messageBoxViewModel.ShowOK = true; + await _dialogService.ShowMessageBoxAsync(messageBoxViewModel); + } } \ No newline at end of file diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 968c0e5c..46e3511d 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -9,6 +9,7 @@ using ByteSync.Business.Sessions; using ByteSync.Common.Business.Inventories; using ByteSync.Common.Business.Misc; +using ByteSync.Helpers; using ByteSync.Interfaces.Controls.Inventories; using ByteSync.Models.FileSystems; using ByteSync.Models.Inventories; @@ -114,6 +115,16 @@ public InventoryPart AddInventoryPart(string fullName) { InventoryPart inventoryPart; + if (ProtectedPaths.TryGetProtectedRoot(fullName, OSPlatform, out var protectedRoot)) + { + _logger.LogWarning( + "InventoryBuilder.AddInventoryPart: Path {Path} is under protected root {ProtectedRoot} and will be rejected", + fullName, protectedRoot); + + throw new InvalidOperationException( + $"Path '{fullName}' is under protected root '{protectedRoot}'"); + } + if (Directory.Exists(fullName)) { inventoryPart = new InventoryPart(Inventory, fullName, FileSystemTypes.Directory); diff --git a/src/ByteSync.Common/Business/Versions/ProtocolVersion.cs b/src/ByteSync.Common/Business/Versions/ProtocolVersion.cs index 146f1421..3e8f8608 100644 --- a/src/ByteSync.Common/Business/Versions/ProtocolVersion.cs +++ b/src/ByteSync.Common/Business/Versions/ProtocolVersion.cs @@ -3,10 +3,11 @@ namespace ByteSync.Common.Business.Versions; public static class ProtocolVersion { public const int V1 = 1; + public const int V2 = 2; - public const int CURRENT = V1; + public const int CURRENT = V2; - public const int MIN_SUPPORTED = V1; + public const int MIN_SUPPORTED = V2; public static bool IsCompatible(int otherVersion) { diff --git a/tests/ByteSync.Client.IntegrationTests/Factories/TestInventoryBuilderFactory.cs b/tests/ByteSync.Client.IntegrationTests/Factories/TestInventoryBuilderFactory.cs index 259d55d6..69c7078d 100644 --- a/tests/ByteSync.Client.IntegrationTests/Factories/TestInventoryBuilderFactory.cs +++ b/tests/ByteSync.Client.IntegrationTests/Factories/TestInventoryBuilderFactory.cs @@ -63,6 +63,7 @@ private void RegisterClientTypes() _builder.RegisterGeneric(typeof(Mock<>)).SingleInstance(); _builder.Register(_ => new Mock>().Object).As>(); + _builder.Register(_ => new Mock>().Object).As>(); _builder.Register(_ => new Mock>().Object).As>(); _builder.Register(_ => new Mock>().Object).As>(); @@ -260,4 +261,4 @@ public void CreateInventoryBuilder_ShouldResolveSharedDependencies() inventoryBuilder2.Should().NotBeNull(); inventoryBuilder1.Should().NotBeSameAs(inventoryBuilder2); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Helpers/ProtectedPathsTests.cs b/tests/ByteSync.Client.UnitTests/Helpers/ProtectedPathsTests.cs new file mode 100644 index 00000000..dfaa9954 --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Helpers/ProtectedPathsTests.cs @@ -0,0 +1,56 @@ +using ByteSync.Common.Business.Misc; +using ByteSync.Helpers; +using FluentAssertions; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Helpers; + +[TestFixture] +public class ProtectedPathsTests +{ + [TestCase("/dev", "/dev")] + [TestCase("/dev/sda", "/dev")] + [TestCase("/proc", "/proc")] + [TestCase("/proc/1", "/proc")] + [TestCase("/sys", "/sys")] + [TestCase("/sys/kernel", "/sys")] + [TestCase("/run", "/run")] + [TestCase("/run/user/1000", "/run")] + [TestCase("/var/run", "/var/run")] + [TestCase("/var/run/daemon", "/var/run")] + public void TryGetProtectedRoot_Linux_ReturnsTrue(string path, string expectedRoot) + { + var result = ProtectedPaths.TryGetProtectedRoot(path, OSPlatforms.Linux, out var root); + + result.Should().BeTrue(); + root.Should().Be(expectedRoot); + } + + [TestCase("/private/var/run", "/private/var/run")] + [TestCase("/private/var/run/daemon", "/private/var/run")] + public void TryGetProtectedRoot_MacOs_ReturnsTrue(string path, string expectedRoot) + { + var result = ProtectedPaths.TryGetProtectedRoot(path, OSPlatforms.MacOs, out var root); + + result.Should().BeTrue(); + root.Should().Be(expectedRoot); + } + + [Test] + public void TryGetProtectedRoot_Linux_WithNonProtectedPath_ReturnsFalse() + { + var result = ProtectedPaths.TryGetProtectedRoot("/home/user", OSPlatforms.Linux, out var root); + + result.Should().BeFalse(); + root.Should().BeEmpty(); + } + + [Test] + public void TryGetProtectedRoot_Windows_ReturnsFalse() + { + var result = ProtectedPaths.TryGetProtectedRoot("C:\\Windows", OSPlatforms.Windows, out var root); + + result.Should().BeFalse(); + root.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/tests/ByteSync.Client.UnitTests/Services/Communications/PublicKeysTrusterTests.cs b/tests/ByteSync.Client.UnitTests/Services/Communications/PublicKeysTrusterTests.cs index 6185833e..f8714a10 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Communications/PublicKeysTrusterTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Communications/PublicKeysTrusterTests.cs @@ -78,7 +78,7 @@ public void SetUp() public async Task OnTrustPublicKeyRequestedAsync_WithIncompatibleProtocolVersion_ShouldThrowInvalidOperationException() { var sessionId = "TestSessionId"; - var incompatibleVersion = 2; + var incompatibleVersion = ProtocolVersion.CURRENT - 1; var joinerInstanceId = "JoinerInstanceId"; var myPublicKeyCheckData = new PublicKeyCheckData @@ -180,7 +180,7 @@ await FluentActions.Invoking(async () => await _publicKeysTruster.OnTrustPublicK public async Task TrustAllMembersPublicKeys_WithIncompatibleProtocolVersion_ShouldReturnIncompatibleProtocolVersion() { var sessionId = "TestSessionId"; - var incompatibleVersion = 2; + var incompatibleVersion = ProtocolVersion.CURRENT - 1; _sessionMemberApiClient .Setup(c => c.GetMembersClientInstanceIds(sessionId, It.IsAny())) @@ -427,7 +427,7 @@ public async Task OnPublicKeyCheckDataAskedAsync_WithIncompatibleProtocolVersion { var sessionId = "TestSessionId"; var clientInstanceId = "ClientInstanceId"; - var incompatibleVersion = 2; + var incompatibleVersion = ProtocolVersion.CURRENT - 1; var publicKeyInfo = new PublicKeyInfo { @@ -565,4 +565,4 @@ public async Task TrustAllMembersPublicKeys_WhenProtocolVersionIncompatibleNotif result.Status.Should().Be(JoinSessionStatus.IncompatibleProtocolVersion); } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/DataSourceCheckerTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/DataSourceCheckerTests.cs index f64fca8e..07b07708 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/DataSourceCheckerTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/DataSourceCheckerTests.cs @@ -1,10 +1,13 @@ -using ByteSync.Business; +using ByteSync.Business; using ByteSync.Business.DataSources; using ByteSync.Common.Business.Inventories; +using ByteSync.Common.Business.Misc; +using ByteSync.Interfaces.Controls.Applications; using ByteSync.Interfaces.Dialogs; using ByteSync.Services.Inventories; using ByteSync.ViewModels.Misc; using FluentAssertions; +using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -12,10 +15,11 @@ namespace ByteSync.Client.UnitTests.Services.Inventories; public class DataSourceCheckerTests { - private Mock _mockDialogService; - private List _existingDataSources; + private Mock _mockDialogService = null!; + private Mock _mockEnvironmentService = null!; + private List _existingDataSources = null!; - private DataSourceChecker _dataSourceChecker; + private DataSourceChecker _dataSourceChecker = null!; [SetUp] public void Setup() @@ -27,9 +31,14 @@ public void Setup() .ReturnsAsync(MessageBoxResult.OK); _mockDialogService .Setup(x => x.CreateMessageBoxViewModel(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((string title, string message, string[] parameters) => new MessageBoxViewModel { ShowOK = true }); + .Returns((string _, string _, string[] _) => new MessageBoxViewModel { ShowOK = true }); - _dataSourceChecker = new DataSourceChecker(_mockDialogService.Object); + _mockEnvironmentService = new Mock(); + _mockEnvironmentService.SetupGet(x => x.ClientInstanceId).Returns("client1"); + _mockEnvironmentService.SetupGet(x => x.OSPlatform).Returns(OSPlatforms.Linux); + var logger = new Mock>().Object; + + _dataSourceChecker = new DataSourceChecker(_mockDialogService.Object, _mockEnvironmentService.Object, logger); _existingDataSources = new List(); } @@ -160,4 +169,36 @@ public async Task CheckDataSource_Directory_ParentOfExisting_ReturnsFalse() result.Should().BeFalse(); _mockDialogService.Verify(x => x.ShowMessageBoxAsync(It.IsAny()), Times.Once); } + + [Test] + public async Task CheckDataSource_ProtectedPath_Local_ReturnsFalse() + { + var dataSource = new DataSource + { + ClientInstanceId = "client1", + Type = FileSystemTypes.Directory, + Path = "/dev" + }; + + var result = await _dataSourceChecker.CheckDataSource(dataSource, _existingDataSources); + + result.Should().BeFalse(); + _mockDialogService.Verify(x => x.ShowMessageBoxAsync(It.IsAny()), Times.Once); + } + + [Test] + public async Task CheckDataSource_ProtectedPath_Remote_ReturnsTrue() + { + var dataSource = new DataSource + { + ClientInstanceId = "client2", + Type = FileSystemTypes.Directory, + Path = "/dev" + }; + + var result = await _dataSourceChecker.CheckDataSource(dataSource, _existingDataSources); + + result.Should().BeTrue(); + _mockDialogService.Verify(x => x.ShowMessageBoxAsync(It.IsAny()), Times.Never); + } } \ No newline at end of file diff --git a/tests/ByteSync.Common.Tests/Business/Versions/ProtocolVersionTests.cs b/tests/ByteSync.Common.Tests/Business/Versions/ProtocolVersionTests.cs index cbd6e1d0..fdd5425a 100644 --- a/tests/ByteSync.Common.Tests/Business/Versions/ProtocolVersionTests.cs +++ b/tests/ByteSync.Common.Tests/Business/Versions/ProtocolVersionTests.cs @@ -8,15 +8,15 @@ namespace TestingCommon.Business.Versions; public class ProtocolVersionTests { [Test] - public void Current_ShouldBeV1() + public void Current_ShouldBeV2() { - ProtocolVersion.CURRENT.Should().Be(ProtocolVersion.V1); + ProtocolVersion.CURRENT.Should().Be(ProtocolVersion.V2); } [Test] - public void MinSupported_ShouldBeV1() + public void MinSupported_ShouldBeV2() { - ProtocolVersion.MIN_SUPPORTED.Should().Be(ProtocolVersion.V1); + ProtocolVersion.MIN_SUPPORTED.Should().Be(ProtocolVersion.V2); } [Test] @@ -28,11 +28,11 @@ public void IsCompatible_WithCurrentVersion_ShouldReturnTrue() } [Test] - public void IsCompatible_WithV1_ShouldReturnTrue() + public void IsCompatible_WithV1_ShouldReturnFalse() { var result = ProtocolVersion.IsCompatible(ProtocolVersion.V1); - result.Should().BeTrue(); + result.Should().BeFalse(); } [Test] @@ -46,7 +46,7 @@ public void IsCompatible_WithZero_ShouldReturnFalse() [Test] public void IsCompatible_WithDifferentVersion_ShouldReturnFalse() { - var result = ProtocolVersion.IsCompatible(2); + var result = ProtocolVersion.IsCompatible(3); result.Should().BeFalse(); }