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();
}