From cc563ba16abaca4d2a60e99ab1e4b44d1d2451b3 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Mon, 4 Nov 2024 00:01:53 +0100 Subject: [PATCH 1/3] Begin work on migrating NWebDav --- .../Desktop/SkiaWebDavFileSystem.Linux.cs | 19 ++----- .../Windows/WindowsWebDavFileSystem.cs | 16 ++---- .../Platforms/Windows/WindowsWebDavVFSRoot.cs | 33 ++++--------- .../AppModels/EncryptingStoreOptions.cs | 9 ++++ .../EncryptingStorage2/EncryptingDiskStore.cs | 20 ++++++-- .../Enums/DavPropertyMode.cs | 13 ----- .../Extensions/BuilderExtensions.cs | 30 ++++++++++++ ...SecureFolderFS - Backup.Core.WebDav.csproj | 24 +++++++++ .../SecureFolderFS.Core.WebDav.csproj | 8 +-- .../WebDavFileSystem.cs | 49 ++++++++++++++----- .../WebDavRootFolder.cs | 26 ++++++---- 11 files changed, 154 insertions(+), 93 deletions(-) create mode 100644 src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs delete mode 100644 src/SecureFolderFS.Core.WebDav/Enums/DavPropertyMode.cs create mode 100644 src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs create mode 100644 src/SecureFolderFS.Core.WebDav/SecureFolderFS - Backup.Core.WebDav.csproj diff --git a/src/Platforms/SecureFolderFS.Uno/Platforms/Desktop/SkiaWebDavFileSystem.Linux.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/Desktop/SkiaWebDavFileSystem.Linux.cs index b6f0b86b0..b6eb888c1 100644 --- a/src/Platforms/SecureFolderFS.Uno/Platforms/Desktop/SkiaWebDavFileSystem.Linux.cs +++ b/src/Platforms/SecureFolderFS.Uno/Platforms/Desktop/SkiaWebDavFileSystem.Linux.cs @@ -20,26 +20,15 @@ internal sealed partial class SkiaWebDavFileSystem #if HAS_UNO_SKIA && !__MACCATALYST__ /// protected override async Task MountAsync( - int port, - string domain, - string protocol, - HttpListener listener, - FileSystemOptions options, - IRequestDispatcher requestDispatcher, + WebDavOptions options, + IAsyncDisposable webDavInstance, CancellationToken cancellationToken) { - var remotePath = DriveMappingHelpers.GetRemotePath(protocol, "localhost", port, options.VolumeName); + var remotePath = DriveMappingHelpers.GetRemotePath(options.Protocol, options.Domain, options.Port, options.VolumeName); var mountPath = await DriveMappingHelpers.GetMountPathForRemotePathAsync(remotePath); - var webDavWrapper = new WebDavWrapper(listener, requestDispatcher, mountPath); - webDavWrapper.StartFileSystem(); - - // TODO: Remove once the port is displayed in the UI. - Debug.WriteLine($"WebDAV server started on port {port}."); - Debug.WriteLine($"MountableDAV\nmountPath: {mountPath}\nremotePath: {remotePath}"); - // TODO: Currently using MemoryFolder because the check in SystemFolder might sometimes fail - return new WebDavRootFolder(webDavWrapper, new MemoryFolder(remotePath, options.VolumeName), options); + return new WebDavRootFolder(webDavInstance, new MemoryFolder(remotePath, options.VolumeName), options); } #endif } diff --git a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs index 618673ba0..b5d9701d9 100644 --- a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs +++ b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs @@ -1,9 +1,7 @@ -using System.Diagnostics; +using System; using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; -using NWebDav.Server.Dispatching; using OwlCore.Storage.Memory; using SecureFolderFS.Core.FileSystem.Helpers; using SecureFolderFS.Core.WebDav; @@ -18,9 +16,8 @@ public sealed class WindowsWebDavFileSystem : WebDavFileSystem { /// protected override async Task MountAsync( - HttpListener listener, WebDavOptions options, - IRequestDispatcher requestDispatcher, + IAsyncDisposable webDavInstance, CancellationToken cancellationToken) { var remotePath = DriveMappingHelpers.GetRemotePath(options.Protocol, "localhost", options.Port, options.VolumeName); @@ -34,14 +31,7 @@ protected override async Task MountAsync( await DriveMappingHelpers.MapNetworkDriveAsync(mountPath, remotePath, cancellationToken); } - var webDavWrapper = new WebDavWrapper(listener, requestDispatcher, mountPath); - webDavWrapper.StartFileSystem(); - - // TODO: Remove once the port is displayed in the UI. - Debug.WriteLine($"WebDAV server started on port {options.Port}."); - - // TODO: Currently using MemoryFolder because the check in SystemFolder might sometimes fail - return new WindowsWebDavVFSRoot(webDavWrapper, new MemoryFolder(remotePath, options.VolumeName), options); + return new WindowsWebDavVFSRoot(webDavInstance, new MemoryFolder(remotePath, options.VolumeName), options); } } } diff --git a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavVFSRoot.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavVFSRoot.cs index c702549c9..5a27ea9e0 100644 --- a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavVFSRoot.cs +++ b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavVFSRoot.cs @@ -1,8 +1,7 @@ using System.Threading.Tasks; using OwlCore.Storage; -using SecureFolderFS.Core.FileSystem; -using SecureFolderFS.Core.WebDav; using SecureFolderFS.Storage.VirtualFileSystem; +using SecureFolderFS.Core.WebDav; #if WINDOWS using System; @@ -15,39 +14,27 @@ namespace SecureFolderFS.Uno.Platforms.Windows { /// - internal sealed class WindowsWebDavVFSRoot : VFSRoot + internal sealed class WindowsWebDavVFSRoot : WebDavRootFolder { - private const uint WM_CLOSE = 0x0010; - - private readonly WebDavWrapper _webDavWrapper; - private bool _disposed; - - /// - public override string FileSystemName { get; } = Core.WebDav.Constants.FileSystem.FS_NAME; - - public WindowsWebDavVFSRoot(WebDavWrapper webDavWrapper, IFolder storageRoot, FileSystemOptions options) - : base(storageRoot, options) + public WindowsWebDavVFSRoot(IAsyncDisposable webDavInstance, IFolder storageRoot, FileSystemOptions options) + : base(webDavInstance, storageRoot, options) { - _webDavWrapper = webDavWrapper; } /// - public override async ValueTask DisposeAsync() + protected override async ValueTask DisposeInternalAsync() { - if (_disposed) - return; + await base.DisposeInternalAsync(); - _disposed = await _webDavWrapper.CloseFileSystemAsync(); - if (_disposed) - { - FileSystemManager.Instance.RemoveRoot(this); - await CloseExplorerShellAsync(Inner.Id); - } + // Close the shell on Windows + await CloseExplorerShellAsync(Inner.Id); } private static async Task CloseExplorerShellAsync(string path) { #if WINDOWS + const uint WM_CLOSE = 0x0010; + try { var formattedPath = PathHelpers.EnsureNoTrailingPathSeparator(path); diff --git a/src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs b/src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs new file mode 100644 index 000000000..4c9a29019 --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs @@ -0,0 +1,9 @@ +using SecureFolderFS.Core.FileSystem; + +namespace SecureFolderFS.Core.WebDav.AppModels +{ + internal sealed class EncryptingStoreOptions + { + public FileSystemSpecifics? Specifics { get; set; } + } +} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs index a5b0741ba..4b08da30f 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs +++ b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs @@ -1,4 +1,4 @@ -using NWebDav.Server.Http; +using Microsoft.Extensions.Logging; using NWebDav.Server.Locking; using NWebDav.Server.Stores; using SecureFolderFS.Core.FileSystem; @@ -9,14 +9,26 @@ namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 { - internal sealed class EncryptingDiskStore : DiskStore + internal sealed class EncryptingDiskStore : DiskStoreBase { private readonly FileSystemSpecifics _specifics; - public EncryptingDiskStore(string directory, FileSystemSpecifics specifics, bool isWritable = true, ILockingManager? lockingManager = null) - : base(directory, isWritable, lockingManager) + /// + public override bool IsWritable { get; } + + /// + public override string BaseDirectory { get; } + + public EncryptingDiskStore( + FileSystemSpecifics specifics, + DiskStoreItemPropertyManager itemPropertyManager, + DiskStoreCollectionPropertyManager collectionPropertyManager, + ILoggerFactory loggerFactory) + : base(collectionPropertyManager, itemPropertyManager, loggerFactory) { _specifics = specifics; + IsWritable = !specifics.FileSystemOptions.IsReadOnly; + BaseDirectory = specifics.ContentFolder.Id; } public override Task GetItemAsync(Uri uri, IHttpContext context) diff --git a/src/SecureFolderFS.Core.WebDav/Enums/DavPropertyMode.cs b/src/SecureFolderFS.Core.WebDav/Enums/DavPropertyMode.cs deleted file mode 100644 index c8cd7ad30..000000000 --- a/src/SecureFolderFS.Core.WebDav/Enums/DavPropertyMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace SecureFolderFS.Core.WebDav.Enums -{ - [Flags] - internal enum DavPropertyMode : uint - { - None = 0, - PropertyNames = 1, - AllProperties = 2, - SelectedProperties = 4 - } -} diff --git a/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs new file mode 100644 index 000000000..a2b3c5596 --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using NWebDav.Server; +using NWebDav.Server.Stores; +using SecureFolderFS.Core.WebDav.AppModels; +using SecureFolderFS.Core.WebDav.EncryptingStorage2; +using System; +using Microsoft.Extensions.Options; + +namespace SecureFolderFS.Core.WebDav.Extensions +{ + internal static class BuilderExtensions + { + public static IServiceCollection AddEncryptingDiskStore(this IServiceCollection services, Action options) + { + return services + .Configure(options) + .AddSingleton() + .AddSingleton() + .AddScoped(sp => + { + var storeOptions = sp.GetService>(); + if (storeOptions?.Value.Specifics is null) + throw new NotSupportedException("Options were not configured."); + + return new(storeOptions.Value.Specifics); + + }); + } + } +} diff --git a/src/SecureFolderFS.Core.WebDav/SecureFolderFS - Backup.Core.WebDav.csproj b/src/SecureFolderFS.Core.WebDav/SecureFolderFS - Backup.Core.WebDav.csproj new file mode 100644 index 000000000..69ab19fea --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/SecureFolderFS - Backup.Core.WebDav.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + disable + enable + AnyCPU;ARM64;x64;x86 + + + + + + + + + + + + + + + + + diff --git a/src/SecureFolderFS.Core.WebDav/SecureFolderFS.Core.WebDav.csproj b/src/SecureFolderFS.Core.WebDav/SecureFolderFS.Core.WebDav.csproj index 22c716f67..9964ec7b8 100644 --- a/src/SecureFolderFS.Core.WebDav/SecureFolderFS.Core.WebDav.csproj +++ b/src/SecureFolderFS.Core.WebDav/SecureFolderFS.Core.WebDav.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,10 @@ - - + + + + diff --git a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs index 8ca438d28..93270903c 100644 --- a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs +++ b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs @@ -1,12 +1,12 @@ -using NWebDav.Server; -using NWebDav.Server.Dispatching; -using NWebDav.Server.Storage; -using NWebDav.Server.Stores; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using NWebDav.Server; using OwlCore.Storage; using SecureFolderFS.Core.Cryptography; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Core.WebDav.AppModels; using SecureFolderFS.Core.WebDav.EncryptingStorage2; +using SecureFolderFS.Core.WebDav.Extensions; using SecureFolderFS.Core.WebDav.Helpers; using SecureFolderFS.Shared.ComponentModel; using SecureFolderFS.Storage.Enums; @@ -48,31 +48,56 @@ public virtual async Task MountAsync(IFolder folder, IDisposable unloc if (!PortHelpers.IsPortAvailable(webDavOptions.Port)) webDavOptions.SetPortInternal(PortHelpers.GetNextAvailablePort(webDavOptions.Port)); - var prefix = $"{webDavOptions.Protocol}://{webDavOptions.Domain}:{webDavOptions.Port}/"; + var url = $"{webDavOptions.Protocol}://{webDavOptions.Domain}:{webDavOptions.Port}/"; + var builder = WebApplication.CreateBuilder(); + builder.Services + .AddNWebDav() + .AddEncryptingDiskStore(x => + { + x.Specifics = specifics; + }); + + var webDavInstance = builder.Build(); + webDavInstance.UseNWebDav(); + _ = webDavInstance.RunAsync(url); + + + return await MountAsync( + httpListener, + webDavOptions, + webDavInstance, + cancellationToken); + + + + + + + var httpListener = new HttpListener(); httpListener.Prefixes.Add(prefix); httpListener.AuthenticationSchemes = AuthenticationSchemes.Anonymous; + + // TODO: Implement FileSystemSpecifics var cryptoFolder = (IFolder)null!; // new CryptoFolder(contentFolder, streamsAccess, pathConverter, directoryIdCache); + var davFolder = new DavFolder(cryptoFolder); + var srv = new ServiceCollection() + .AddNWebDav() + // TODO: Remove the following line once the new DavStorage is fully implemented. var encryptingDiskStore = new EncryptingDiskStore(specifics.ContentFolder.Id, specifics, !specifics.FileSystemOptions.IsReadOnly); var dispatcher = new WebDavDispatcher(new RootDiskStore(specifics.FileSystemOptions.VolumeName, encryptingDiskStore), davFolder, new RequestHandlerProvider(), null); - - return await MountAsync( - httpListener, - webDavOptions, - dispatcher, - cancellationToken); } protected abstract Task MountAsync( HttpListener listener, WebDavOptions options, - IRequestDispatcher requestDispatcher, + IAsyncDisposable webDavInstance, CancellationToken cancellationToken); } } diff --git a/src/SecureFolderFS.Core.WebDav/WebDavRootFolder.cs b/src/SecureFolderFS.Core.WebDav/WebDavRootFolder.cs index 1d565297a..b738a4b82 100644 --- a/src/SecureFolderFS.Core.WebDav/WebDavRootFolder.cs +++ b/src/SecureFolderFS.Core.WebDav/WebDavRootFolder.cs @@ -1,34 +1,40 @@ using OwlCore.Storage; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Storage.VirtualFileSystem; +using System; using System.Threading.Tasks; namespace SecureFolderFS.Core.WebDav { /// - public sealed class WebDavRootFolder : VFSRoot + public class WebDavRootFolder : VFSRoot { - private readonly WebDavWrapper _webDavWrapper; - private bool _disposed; + protected readonly IAsyncDisposable webDavInstance; + protected bool disposed; /// public override string FileSystemName { get; } = Constants.FileSystem.FS_NAME; - public WebDavRootFolder(WebDavWrapper webDavWrapper, IFolder storageRoot, FileSystemOptions options) + public WebDavRootFolder(IAsyncDisposable webDavInstance, IFolder storageRoot, FileSystemOptions options) : base(storageRoot, options) { - _webDavWrapper = webDavWrapper; + this.webDavInstance = webDavInstance; } /// - public override async ValueTask DisposeAsync() + public sealed override async ValueTask DisposeAsync() { - if (_disposed) + if (disposed) return; - _disposed = await _webDavWrapper.CloseFileSystemAsync(); - if (_disposed) - FileSystemManager.Instance.RemoveRoot(this); + disposed = true; + await DisposeInternalAsync(); + } + + protected virtual async ValueTask DisposeInternalAsync() + { + await webDavInstance.DisposeAsync(); + FileSystemManager.Instance.RemoveRoot(this); } } } From 3c47c83915d6210982b0c9b66c06c4a501abe0ad Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Mon, 4 Nov 2024 01:08:17 +0100 Subject: [PATCH 2/3] Some more work --- .../Windows/WindowsWebDavFileSystem.cs | 2 +- .../Base/DiskStoreBase2.cs | 98 ++++++++ .../EncryptingStorage2/EncryptingDiskStore.cs | 51 ++--- .../EncryptingDiskStoreCollection.cs | 2 +- .../EncryptingDiskStoreItem.cs | 213 +++++------------- .../Extensions/BuilderExtensions.cs | 9 +- .../WebDavFileSystem.cs | 23 -- 7 files changed, 181 insertions(+), 217 deletions(-) create mode 100644 src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs diff --git a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs index b5d9701d9..d55c2c68b 100644 --- a/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs +++ b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs @@ -20,7 +20,7 @@ protected override async Task MountAsync( IAsyncDisposable webDavInstance, CancellationToken cancellationToken) { - var remotePath = DriveMappingHelpers.GetRemotePath(options.Protocol, "localhost", options.Port, options.VolumeName); + var remotePath = DriveMappingHelpers.GetRemotePath(options.Protocol, options.Domain, options.Port, options.VolumeName); var mountPath = await DriveMappingHelpers.GetMountPathForRemotePathAsync(remotePath); if (mountPath is null) { diff --git a/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs b/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs new file mode 100644 index 000000000..ab54a59ec --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Logging; +using NWebDav.Server.Stores; +using System; +using System.IO; +using System.Security; +using System.Threading; +using System.Threading.Tasks; + +namespace SecureFolderFS.Core.WebDav.Base +{ + internal abstract class DiskStoreBase2 : IStore + { + private readonly DiskStoreCollectionPropertyManager _diskStoreCollectionPropertyManager; + private readonly DiskStoreItemPropertyManager _diskStoreItemPropertyManager; + private readonly ILoggerFactory _loggerFactory; + + public abstract bool IsWritable { get; } + + public abstract string BaseDirectory { get; } + + protected DiskStoreBase2(DiskStoreCollectionPropertyManager diskStoreCollectionPropertyManager, DiskStoreItemPropertyManager diskStoreItemPropertyManager, ILoggerFactory loggerFactory) + { + _diskStoreCollectionPropertyManager = diskStoreCollectionPropertyManager; + _diskStoreItemPropertyManager = diskStoreItemPropertyManager; + _loggerFactory = loggerFactory; + } + + public virtual async Task GetItemAsync(Uri uri, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Get path and item + var path = GetPathFromUri(uri); + var item = CreateFromPath(path); + + return item; + } + + public virtual async Task GetCollectionAsync(Uri uri, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Determine the path from the uri + var path = GetPathFromUri(uri); + if (!Directory.Exists(path)) + return null; + + // Return the item + return CreateFromPath(path); + } + + protected virtual string GetPathFromUri(Uri uri) + { + // Determine the path + var requestedPath = UriHelper.GetDecodedPath(uri)[1..].Replace('/', Path.DirectorySeparatorChar); + + // Determine the full path + var fullPath = Path.GetFullPath(Path.Combine(BaseDirectory, requestedPath)); + + // Make sure we're still inside the specified directory + if (fullPath != BaseDirectory && !fullPath.StartsWith(BaseDirectory + Path.DirectorySeparatorChar)) + throw new SecurityException($"Uri '{uri}' is outside the '{BaseDirectory}' directory."); + + // Return the combined path + return fullPath; + } + + public virtual T? CreateFromPath(string path) + where T : class, IStoreItem + { + if (typeof(T).IsAssignableFrom(typeof(IStoreCollection))) + { + return (T?)(object)CreateCollection(new DirectoryInfo(path)); + } + //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add a StoreFile + else + { + // Check if it's a directory + if (Directory.Exists(path)) + return (T?)(object)CreateCollection(new DirectoryInfo(path)); + + // Check if it's a file + if (File.Exists(path)) + return (T?)(object)CreateItem(new FileInfo(path)); + + // The item doesn't exist + return null; + } + + internal DiskStoreCollection CreateCollection(DirectoryInfo directoryInfo) => + new(this, _diskStoreCollectionPropertyManager, directoryInfo, _loggerFactory.CreateLogger()); + + internal DiskStoreItem CreateItem(FileInfo file) => + new(this, _diskStoreItemPropertyManager, file, _loggerFactory.CreateLogger()); + } +} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs index 4b08da30f..c092377a6 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs +++ b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs @@ -1,15 +1,14 @@ using Microsoft.Extensions.Logging; -using NWebDav.Server.Locking; using NWebDav.Server.Stores; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Core.FileSystem.Helpers.Native; +using SecureFolderFS.Core.WebDav.Base; using System; using System.IO; -using System.Threading.Tasks; namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 { - internal sealed class EncryptingDiskStore : DiskStoreBase + internal sealed class EncryptingDiskStore : DiskStoreBase2 { private readonly FileSystemSpecifics _specifics; @@ -31,34 +30,30 @@ public EncryptingDiskStore( BaseDirectory = specifics.ContentFolder.Id; } - public override Task GetItemAsync(Uri uri, IHttpContext context) + public override T? CreateFromPath(string path) + where T : class { - // Determine the path from the uri - var path = GetPathFromUri(uri); - - // Check if it's a directory - if (Directory.Exists(path)) - return Task.FromResult(new EncryptingDiskStoreCollection(LockingManager, new DirectoryInfo(path), IsWritable, _specifics)); - - // Check if it's a file - if (File.Exists(path)) - return Task.FromResult(new EncryptingDiskStoreItem(LockingManager, new FileInfo(path), IsWritable, _specifics)); - - // The item doesn't exist - return Task.FromResult(null); - } - - public override Task GetCollectionAsync(Uri uri, IHttpContext context) - { - // Determine the path from the uri - var path = GetPathFromUri(uri); - if (!Directory.Exists(path)) - return Task.FromResult(null); - - // Return the item - return Task.FromResult(new EncryptingDiskStoreCollection(LockingManager, new DirectoryInfo(path), IsWritable, _specifics)); + if (typeof(T).IsAssignableFrom(typeof(IStoreCollection))) + { + return (T?)(object)new EncryptingDiskStoreCollection(new DirectoryInfo(path)); + } + //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add a StoreFile + else + { + // Check if it's a directory + if (Directory.Exists(path)) + return (T?)(object)new EncryptingDiskStoreCollection(new DirectoryInfo(path)); + + // Check if it's a file + if (File.Exists(path)) + return (T?)(object)new EncryptingDiskStoreItem(new FileInfo(path)); + + // The item doesn't exist + return null; + } } + /// protected override string GetPathFromUri(Uri uri) { var path = base.GetPathFromUri(uri); diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs index 75530723f..b41a49f64 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs +++ b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs @@ -18,7 +18,7 @@ namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 { - internal sealed class EncryptingDiskStoreCollection : IDiskStoreCollection + internal sealed class EncryptingDiskStoreCollection : IStoreCollection { private static readonly XElement s_xDavCollection = new XElement(WebDavNamespaces.DavNs + "collection"); private readonly DirectoryInfo _directoryInfo; diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs index d3d7ae0d1..487e62917 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs +++ b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs @@ -1,229 +1,118 @@ -using NWebDav.Server.Helpers; -using NWebDav.Server.Http; -using NWebDav.Server.Locking; +using Microsoft.Extensions.Logging; +using NWebDav.Server; +using NWebDav.Server.Helpers; using NWebDav.Server.Props; using NWebDav.Server.Stores; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Core.FileSystem.Helpers.Native; using System; using System.IO; -using System.Net; +using System.Threading; using System.Threading.Tasks; namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 { - internal class EncryptingDiskStoreItem : IDiskStoreItem + internal class EncryptingDiskStoreItem : IStoreItem { private readonly FileSystemSpecifics _specifics; - private readonly FileInfo _fileInfo; + private readonly ILogger _logger; - public EncryptingDiskStoreItem(ILockingManager lockingManager, FileInfo fileInfo, bool isWritable, FileSystemSpecifics specifics) - { - LockingManager = lockingManager; - IsWritable = isWritable; - _fileInfo = fileInfo; - _specifics = specifics; - } + /// + public string Name { get; } - public static PropertyManager DefaultPropertyManager { get; } = new(new DavProperty[] - { - // RFC-2518 properties - new DavCreationDate - { - Getter = (context, item) => item._fileInfo.CreationTimeUtc, - Setter = (context, item, value) => - { - item._fileInfo.CreationTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new DavDisplayName - { - Getter = (context, item) => item.Name - }, - new DavGetContentLength - { - Getter = (context, item) => Math.Max(0, item._specifics.Security.ContentCrypt.CalculatePlaintextSize(item._fileInfo.Length - item._specifics.Security.HeaderCrypt.HeaderCiphertextSize)) - }, - new DavGetContentType - { - Getter = (context, item) => item.DetermineContentType() - }, - new DavGetEtag - { - Getter = (context, item) => $"{item._fileInfo.Length}-{item._fileInfo.LastWriteTimeUtc.ToFileTime()}" - }, - new DavGetLastModified - { - Getter = (context, item) => item._fileInfo.LastWriteTimeUtc, - Setter = (context, item, value) => - { - item._fileInfo.LastWriteTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new DavGetResourceType - { - Getter = (context, item) => null - }, + /// + public string UniqueKey { get; } - // Default locking property handling via the LockingManager - new DavLockDiscoveryDefault(), - new DavSupportedLockDefault(), + /// + public IPropertyManager PropertyManager { get; } - // Hopmann/Lippert collection properties - // (although not a collection, the IsHidden property might be valuable) - new DavExtCollectionIsHidden - { - Getter = (context, item) => (item._fileInfo.Attributes & FileAttributes.Hidden) != 0 - }, + public FileInfo FileInfo { get; } // TODO: Not from interface + public bool IsWritable { get; } // TODO: Not from interface - // Win32 extensions - new Win32CreationTime - { - Getter = (context, item) => item._fileInfo.CreationTimeUtc, - Setter = (context, item, value) => - { - item._fileInfo.CreationTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32LastAccessTime - { - Getter = (context, item) => item._fileInfo.LastAccessTimeUtc, - Setter = (context, item, value) => - { - item._fileInfo.LastAccessTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32LastModifiedTime - { - Getter = (context, item) => item._fileInfo.LastWriteTimeUtc, - Setter = (context, item, value) => - { - item._fileInfo.LastWriteTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32FileAttributes - { - Getter = (context, item) => item._fileInfo.Attributes, - Setter = (context, item, value) => - { - item._fileInfo.Attributes = value; - return HttpStatusCode.OK; - } - } - }); + public EncryptingDiskStoreItem(FileInfo fileInfo, DiskStoreItemPropertyManager propertyManager, FileSystemSpecifics specifics, ILogger logger) + { + _logger = logger; + _specifics = specifics; + IsWritable = !specifics.FileSystemOptions.IsReadOnly; + UniqueKey = NativePathHelpers.GetPlaintextPath(fileInfo.FullName, specifics) ?? string.Empty; + Name = Path.GetFileName(UniqueKey); + FileInfo = fileInfo; + PropertyManager = propertyManager; + } - public bool IsWritable { get; } - public string Name => NativePathHelpers.GetPlaintextPath(_fileInfo.FullName, _specifics) ?? string.Empty; - public string UniqueKey => _fileInfo.FullName; - public string FullPath => NativePathHelpers.GetPlaintextPath(_fileInfo.FullName, _specifics) ?? string.Empty; - public Task GetReadableStreamAsync(IHttpContext context) => Task.FromResult(_specifics.StreamsAccess.OpenPlaintextStream(_fileInfo.FullName, _fileInfo.OpenRead())); - public Task GetWritableStreamAsync(IHttpContext context) => Task.FromResult(_specifics.StreamsAccess.OpenPlaintextStream(_fileInfo.FullName, _fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite))); + /// + public async Task GetReadableStreamAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + return _specifics.StreamsAccess.OpenPlaintextStream(FileInfo.FullName, FileInfo.OpenRead()); + } - public async Task UploadFromStreamAsync(IHttpContext context, Stream inputStream) + /// + public async Task UploadFromStreamAsync(Stream inputStream, CancellationToken cancellationToken) { // Check if the item is writable if (!IsWritable) - return HttpStatusCode.Forbidden; + return DavStatusCode.Conflict; - // Copy the stream try { // Copy the information to the destination stream - using (var outputStream = await GetWritableStreamAsync(context).ConfigureAwait(false)) + var outputStream = _specifics.StreamsAccess.OpenPlaintextStream(FileInfo.FullName, FileInfo.OpenWrite()); + await using (outputStream.ConfigureAwait(false)) { - await inputStream.CopyToAsync(outputStream).ConfigureAwait(false); + // Copy the stream + await inputStream.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); } - return HttpStatusCode.OK; + return DavStatusCode.Ok; } catch (IOException ioException) when (ioException.IsDiskFull()) { - return HttpStatusCode.InsufficientStorage; - } - catch (Exception ex) - { - _ = ex; - throw; + return DavStatusCode.InsufficientStorage; } } - public IPropertyManager PropertyManager => DefaultPropertyManager; - public ILockingManager LockingManager { get; } - - public async Task CopyAsync(IStoreCollection destination, string name, bool overwrite, IHttpContext context) + /// + public async Task CopyAsync(IStoreCollection destination, string name, bool overwrite, CancellationToken cancellationToken) { try { // If the destination is also a disk-store, then we can use the FileCopy API // (it's probably a bit more efficient than copying in C#) - if (destination is DiskStoreCollection diskCollection) + if (destination is EncryptingDiskStoreCollection diskCollection) { // Check if the collection is writable if (!diskCollection.IsWritable) - return new StoreItemResult(HttpStatusCode.Forbidden); + return new StoreItemResult(DavStatusCode.PreconditionFailed); var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(diskCollection.FullPath, name), _specifics); // Check if the file already exists var fileExists = File.Exists(destinationPath); if (fileExists && !overwrite) - return new StoreItemResult(HttpStatusCode.PreconditionFailed); + return new StoreItemResult(DavStatusCode.PreconditionFailed); // Copy the file - File.Copy(_fileInfo.FullName, destinationPath, true); + File.Copy(FileInfo.FullName, destinationPath, true); // Return the appropriate status - return new StoreItemResult(fileExists ? HttpStatusCode.NoContent : HttpStatusCode.Created); + return new StoreItemResult(fileExists ? DavStatusCode.NoContent : DavStatusCode.Created); } else { // Create the item in the destination collection - var result = await destination.CreateItemAsync(name, overwrite, context).ConfigureAwait(false); - - // Check if the item could be created - if (result.Item != null) + var sourceStream = await GetReadableStreamAsync(cancellationToken).ConfigureAwait(false); + await using (sourceStream.ConfigureAwait(false)) { - using (var sourceStream = await GetWritableStreamAsync(context).ConfigureAwait(false)) - { - var copyResult = await result.Item.UploadFromStreamAsync(context, sourceStream).ConfigureAwait(false); - if (copyResult != HttpStatusCode.OK) - return new StoreItemResult(copyResult, result.Item); - } + return await destination.CreateItemAsync(name, sourceStream, overwrite, cancellationToken).ConfigureAwait(false); } - - // Return result - return new StoreItemResult(result.Result, result.Item); } } catch (Exception exc) { - // TODO(wd): Add logging - //s_log.Log(LogLevel.Error, () => "Unexpected exception while copying data.", exc); - return new StoreItemResult(HttpStatusCode.InternalServerError); + _logger.LogError(exc, "Unexpected exception while copying data."); + return new StoreItemResult(DavStatusCode.InternalServerError); } } - - public override int GetHashCode() - { - return _fileInfo.FullName.GetHashCode(); - } - - public override bool Equals(object? obj) - { - if (obj is not EncryptingDiskStoreItem storeItem) - return false; - - return storeItem._fileInfo.FullName.Equals(_fileInfo.FullName, StringComparison.CurrentCultureIgnoreCase); - } - - private string DetermineContentType() - { - return MimeTypeHelper.GetMimeType(Name); - } } } diff --git a/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs index a2b3c5596..385ad7231 100644 --- a/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs +++ b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs @@ -4,6 +4,7 @@ using SecureFolderFS.Core.WebDav.AppModels; using SecureFolderFS.Core.WebDav.EncryptingStorage2; using System; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace SecureFolderFS.Core.WebDav.Extensions @@ -14,15 +15,19 @@ public static IServiceCollection AddEncryptingDiskStore(this IServiceCollection { return services .Configure(options) - .AddSingleton() .AddSingleton() + .AddSingleton() .AddScoped(sp => { var storeOptions = sp.GetService>(); if (storeOptions?.Value.Specifics is null) throw new NotSupportedException("Options were not configured."); - return new(storeOptions.Value.Specifics); + var itemPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreItemPropertyManager)); + var collectionPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreCollectionPropertyManager)); + var loggerFactory = sp.GetService() ?? throw new ArgumentNullException(nameof(ILoggerFactory)); + + return new(storeOptions.Value.Specifics, itemPropertyManager, collectionPropertyManager, loggerFactory); }); } diff --git a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs index 93270903c..6034d55fc 100644 --- a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs +++ b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs @@ -61,41 +61,18 @@ public virtual async Task MountAsync(IFolder folder, IDisposable unloc webDavInstance.UseNWebDav(); _ = webDavInstance.RunAsync(url); - return await MountAsync( - httpListener, webDavOptions, webDavInstance, cancellationToken); - - - - - - var httpListener = new HttpListener(); - - httpListener.Prefixes.Add(prefix); - httpListener.AuthenticationSchemes = AuthenticationSchemes.Anonymous; - - - - // TODO: Implement FileSystemSpecifics - var cryptoFolder = (IFolder)null!; // new CryptoFolder(contentFolder, streamsAccess, pathConverter, directoryIdCache); - - var davFolder = new DavFolder(cryptoFolder); - - var srv = new ServiceCollection() - .AddNWebDav() - // TODO: Remove the following line once the new DavStorage is fully implemented. var encryptingDiskStore = new EncryptingDiskStore(specifics.ContentFolder.Id, specifics, !specifics.FileSystemOptions.IsReadOnly); var dispatcher = new WebDavDispatcher(new RootDiskStore(specifics.FileSystemOptions.VolumeName, encryptingDiskStore), davFolder, new RequestHandlerProvider(), null); } protected abstract Task MountAsync( - HttpListener listener, WebDavOptions options, IAsyncDisposable webDavInstance, CancellationToken cancellationToken); From 7736a2340e38b5aa9b6294ef49e5b6ef783388cf Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:24:27 +0100 Subject: [PATCH 3/3] Finish DiskStoreCollection --- .../MacCatalyst/MacOsWebDavFileSystem.cs | 11 +- ...gStoreOptions.cs => CipherStoreOptions.cs} | 2 +- .../Base/DiskStoreBase2.cs | 98 ---- .../EncryptingDiskStoreCollection.cs | 462 ------------------ .../Extensions/BuilderExtensions.cs | 22 +- .../CipherStore.cs} | 36 +- .../Store/CipherStoreBase.cs | 74 +++ .../Store/CipherStoreCollection.cs | 326 ++++++++++++ .../CipherStoreItem.cs} | 16 +- .../WebDavFileSystem.cs | 20 +- .../WebDavWrapper.cs | 66 --- 11 files changed, 446 insertions(+), 687 deletions(-) rename src/SecureFolderFS.Core.WebDav/AppModels/{EncryptingStoreOptions.cs => CipherStoreOptions.cs} (77%) delete mode 100644 src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs delete mode 100644 src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs rename src/SecureFolderFS.Core.WebDav/{EncryptingStorage2/EncryptingDiskStore.cs => Store/CipherStore.cs} (64%) create mode 100644 src/SecureFolderFS.Core.WebDav/Store/CipherStoreBase.cs create mode 100644 src/SecureFolderFS.Core.WebDav/Store/CipherStoreCollection.cs rename src/SecureFolderFS.Core.WebDav/{EncryptingStorage2/EncryptingDiskStoreItem.cs => Store/CipherStoreItem.cs} (89%) delete mode 100644 src/SecureFolderFS.Core.WebDav/WebDavWrapper.cs diff --git a/src/Platforms/SecureFolderFS.Uno/Platforms/MacCatalyst/MacOsWebDavFileSystem.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/MacCatalyst/MacOsWebDavFileSystem.cs index ae1ec38cc..304b93b40 100644 --- a/src/Platforms/SecureFolderFS.Uno/Platforms/MacCatalyst/MacOsWebDavFileSystem.cs +++ b/src/Platforms/SecureFolderFS.Uno/Platforms/MacCatalyst/MacOsWebDavFileSystem.cs @@ -3,7 +3,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using NWebDav.Server.Dispatching; using SecureFolderFS.Core.WebDav; using SecureFolderFS.Core.WebDav.AppModels; using SecureFolderFS.Storage.VirtualFileSystem; @@ -20,9 +19,8 @@ namespace SecureFolderFS.Uno.Platforms.Desktop internal sealed partial class MacOsWebDavFileSystem : WebDavFileSystem { protected override async Task MountAsync( - HttpListener listener, WebDavOptions options, - IRequestDispatcher requestDispatcher, + IAsyncDisposable webDavInstance, CancellationToken cancellationToken) { #if __MACCATALYST__ @@ -32,14 +30,9 @@ protected override async Task MountAsync( // Mount WebDAV volume via AppleScript Process.Start("/usr/bin/osascript", ["-e", $"mount volume \"{remoteUri.AbsoluteUri}\""]); var mountPoint = $"/Volumes/{options.VolumeName}"; - - // Create wrapper - var webDavWrapper = new WebDavWrapper(listener, requestDispatcher, mountPoint); - webDavWrapper.StartFileSystem(); - Debug.WriteLine($"Mounted {remoteUri} on {mountPoint}."); await Task.CompletedTask; - return new WebDavRootFolder(webDavWrapper, new MemoryFolder(mountPoint, options.VolumeName), options); + return new WebDavRootFolder(webDavInstance, new MemoryFolder(mountPoint, options.VolumeName), options); #else throw new PlatformNotSupportedException(); #endif diff --git a/src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs b/src/SecureFolderFS.Core.WebDav/AppModels/CipherStoreOptions.cs similarity index 77% rename from src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs rename to src/SecureFolderFS.Core.WebDav/AppModels/CipherStoreOptions.cs index 4c9a29019..678f41813 100644 --- a/src/SecureFolderFS.Core.WebDav/AppModels/EncryptingStoreOptions.cs +++ b/src/SecureFolderFS.Core.WebDav/AppModels/CipherStoreOptions.cs @@ -2,7 +2,7 @@ namespace SecureFolderFS.Core.WebDav.AppModels { - internal sealed class EncryptingStoreOptions + internal sealed class CipherStoreOptions { public FileSystemSpecifics? Specifics { get; set; } } diff --git a/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs b/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs deleted file mode 100644 index ab54a59ec..000000000 --- a/src/SecureFolderFS.Core.WebDav/Base/DiskStoreBase2.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Microsoft.Extensions.Logging; -using NWebDav.Server.Stores; -using System; -using System.IO; -using System.Security; -using System.Threading; -using System.Threading.Tasks; - -namespace SecureFolderFS.Core.WebDav.Base -{ - internal abstract class DiskStoreBase2 : IStore - { - private readonly DiskStoreCollectionPropertyManager _diskStoreCollectionPropertyManager; - private readonly DiskStoreItemPropertyManager _diskStoreItemPropertyManager; - private readonly ILoggerFactory _loggerFactory; - - public abstract bool IsWritable { get; } - - public abstract string BaseDirectory { get; } - - protected DiskStoreBase2(DiskStoreCollectionPropertyManager diskStoreCollectionPropertyManager, DiskStoreItemPropertyManager diskStoreItemPropertyManager, ILoggerFactory loggerFactory) - { - _diskStoreCollectionPropertyManager = diskStoreCollectionPropertyManager; - _diskStoreItemPropertyManager = diskStoreItemPropertyManager; - _loggerFactory = loggerFactory; - } - - public virtual async Task GetItemAsync(Uri uri, CancellationToken cancellationToken) - { - await Task.CompletedTask; - cancellationToken.ThrowIfCancellationRequested(); - - // Get path and item - var path = GetPathFromUri(uri); - var item = CreateFromPath(path); - - return item; - } - - public virtual async Task GetCollectionAsync(Uri uri, CancellationToken cancellationToken) - { - await Task.CompletedTask; - cancellationToken.ThrowIfCancellationRequested(); - - // Determine the path from the uri - var path = GetPathFromUri(uri); - if (!Directory.Exists(path)) - return null; - - // Return the item - return CreateFromPath(path); - } - - protected virtual string GetPathFromUri(Uri uri) - { - // Determine the path - var requestedPath = UriHelper.GetDecodedPath(uri)[1..].Replace('/', Path.DirectorySeparatorChar); - - // Determine the full path - var fullPath = Path.GetFullPath(Path.Combine(BaseDirectory, requestedPath)); - - // Make sure we're still inside the specified directory - if (fullPath != BaseDirectory && !fullPath.StartsWith(BaseDirectory + Path.DirectorySeparatorChar)) - throw new SecurityException($"Uri '{uri}' is outside the '{BaseDirectory}' directory."); - - // Return the combined path - return fullPath; - } - - public virtual T? CreateFromPath(string path) - where T : class, IStoreItem - { - if (typeof(T).IsAssignableFrom(typeof(IStoreCollection))) - { - return (T?)(object)CreateCollection(new DirectoryInfo(path)); - } - //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add a StoreFile - else - { - // Check if it's a directory - if (Directory.Exists(path)) - return (T?)(object)CreateCollection(new DirectoryInfo(path)); - - // Check if it's a file - if (File.Exists(path)) - return (T?)(object)CreateItem(new FileInfo(path)); - - // The item doesn't exist - return null; - } - - internal DiskStoreCollection CreateCollection(DirectoryInfo directoryInfo) => - new(this, _diskStoreCollectionPropertyManager, directoryInfo, _loggerFactory.CreateLogger()); - - internal DiskStoreItem CreateItem(FileInfo file) => - new(this, _diskStoreItemPropertyManager, file, _loggerFactory.CreateLogger()); - } -} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs deleted file mode 100644 index b41a49f64..000000000 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs +++ /dev/null @@ -1,462 +0,0 @@ -using NWebDav.Server; -using NWebDav.Server.Enums; -using NWebDav.Server.Http; -using NWebDav.Server.Locking; -using NWebDav.Server.Props; -using NWebDav.Server.Stores; -using SecureFolderFS.Core.FileSystem; -using SecureFolderFS.Core.FileSystem.Helpers; -using SecureFolderFS.Core.FileSystem.Helpers.Native; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 -{ - internal sealed class EncryptingDiskStoreCollection : IStoreCollection - { - private static readonly XElement s_xDavCollection = new XElement(WebDavNamespaces.DavNs + "collection"); - private readonly DirectoryInfo _directoryInfo; - private readonly FileSystemSpecifics _specifics; - - public EncryptingDiskStoreCollection(ILockingManager lockingManager, DirectoryInfo directoryInfo, bool isWritable, FileSystemSpecifics specifics) - { - LockingManager = lockingManager; - _directoryInfo = directoryInfo; - IsWritable = isWritable; - _specifics = specifics; - } - - public static PropertyManager DefaultPropertyManager { get; } = new(new DavProperty[] - { - // RFC-2518 properties - new DavCreationDate - { - Getter = (context, collection) => collection._directoryInfo.CreationTimeUtc, - Setter = (context, collection, value) => - { - collection._directoryInfo.CreationTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new DavDisplayName - { - Getter = (context, collection) => - { - return collection._directoryInfo.Name == "content" - // Return the name of the root directory (Name will throw, as the content folder doesn't have a DirectoryID) - ? context.Request.Url.Segments[1] - : collection.Name; - } - }, - new DavGetLastModified - { - Getter = (context, collection) => collection._directoryInfo.LastWriteTimeUtc, - Setter = (context, collection, value) => - { - collection._directoryInfo.LastWriteTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new DavGetResourceType - { - Getter = (context, collection) => new []{s_xDavCollection} - }, - - // Default locking property handling via the LockingManager - new DavLockDiscoveryDefault(), - new DavSupportedLockDefault(), - - // Hopmann/Lippert collection properties - new DavExtCollectionChildCount - { - Getter = (context, collection) => collection._directoryInfo.EnumerateFiles().Count() + collection._directoryInfo.EnumerateDirectories().Count() - }, - new DavExtCollectionIsFolder - { - Getter = (context, collection) => true - }, - new DavExtCollectionIsHidden - { - Getter = (context, collection) => (collection._directoryInfo.Attributes & FileAttributes.Hidden) != 0 - }, - new DavExtCollectionIsStructuredDocument - { - Getter = (context, collection) => false - }, - new DavExtCollectionHasSubs - { - Getter = (context, collection) => collection._directoryInfo.EnumerateDirectories().Any() - }, - new DavExtCollectionNoSubs - { - Getter = (context, collection) => false - }, - new DavExtCollectionObjectCount - { - Getter = (context, collection) => collection._directoryInfo.EnumerateFiles().Count() - }, - new DavExtCollectionReserved - { - Getter = (context, collection) => !collection.IsWritable - }, - new DavExtCollectionVisibleCount - { - Getter = (context, collection) => - collection._directoryInfo.EnumerateDirectories().Count(di => (di.Attributes & FileAttributes.Hidden) == 0) + - collection._directoryInfo.EnumerateFiles().Count(fi => (fi.Attributes & FileAttributes.Hidden) == 0) - }, - - // Win32 extensions - new Win32CreationTime - { - Getter = (context, collection) => collection._directoryInfo.CreationTimeUtc, - Setter = (context, collection, value) => - { - collection._directoryInfo.CreationTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32LastAccessTime - { - Getter = (context, collection) => collection._directoryInfo.LastAccessTimeUtc, - Setter = (context, collection, value) => - { - collection._directoryInfo.LastAccessTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32LastModifiedTime - { - Getter = (context, collection) => collection._directoryInfo.LastWriteTimeUtc, - Setter = (context, collection, value) => - { - collection._directoryInfo.LastWriteTimeUtc = value; - return HttpStatusCode.OK; - } - }, - new Win32FileAttributes - { - Getter = (context, collection) => collection._directoryInfo.Attributes, - Setter = (context, collection, value) => - { - collection._directoryInfo.Attributes = value; - return HttpStatusCode.OK; - } - } - }); - - public bool IsWritable { get; } - public string Name => NativePathHelpers.GetPlaintextPath(_directoryInfo.FullName, _specifics) ?? string.Empty; - public string UniqueKey => _directoryInfo.FullName; - public string FullPath => NativePathHelpers.GetPlaintextPath(_directoryInfo.FullName, _specifics) ?? string.Empty; - - // Disk collections (a.k.a. directories don't have their own data) - public Task GetReadableStreamAsync(IHttpContext context) => Task.FromResult((Stream)null); - public Task UploadFromStreamAsync(IHttpContext context, Stream inputStream) => Task.FromResult(HttpStatusCode.Conflict); - - public IPropertyManager PropertyManager => DefaultPropertyManager; - public ILockingManager LockingManager { get; } - - public Task GetItemAsync(string name, IHttpContext context) - { - // Determine the full path - var fullPath = NativePathHelpers.GetCiphertextPath(Path.Combine(FullPath, name), _specifics); - - // Check if the item is a file - if (File.Exists(fullPath)) - return Task.FromResult(new EncryptingDiskStoreItem(LockingManager, new FileInfo(fullPath), IsWritable, _specifics)); - - // Check if the item is a directory - if (Directory.Exists(fullPath)) - return Task.FromResult(new EncryptingDiskStoreCollection(LockingManager, new DirectoryInfo(fullPath), IsWritable, _specifics)); - - // Item not found - return Task.FromResult(null); - } - - public Task> GetItemsAsync(IHttpContext context) - { - IEnumerable GetItemsInternal() - { - // Add all directories - foreach (var subDirectory in _directoryInfo.GetDirectories()) - { - if (PathHelpers.IsCoreFile(subDirectory.Name)) - continue; - - yield return new EncryptingDiskStoreCollection(LockingManager, subDirectory, IsWritable, _specifics); - } - - // Add all files - foreach (var file in _directoryInfo.GetFiles()) - { - if (PathHelpers.IsCoreFile(file.Name)) - continue; - - yield return new EncryptingDiskStoreItem(LockingManager, file, IsWritable, _specifics); - } - } - - return Task.FromResult(GetItemsInternal()); - } - - public Task CreateItemAsync(string name, bool overwrite, IHttpContext context) - { - // Return error - if (!IsWritable) - return Task.FromResult(new StoreItemResult(HttpStatusCode.Forbidden)); - - // Determine the destination path - var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(FullPath, name), _specifics); - - // Determine result - HttpStatusCode result; - - // Check if the file can be overwritten - if (File.Exists(name)) - { - if (!overwrite) - return Task.FromResult(new StoreItemResult(HttpStatusCode.PreconditionFailed)); - - result = HttpStatusCode.NoContent; - } - else - { - result = HttpStatusCode.Created; - } - - try - { - // Create a new file - File.Create(destinationPath).Dispose(); - } - catch (Exception exc) - { - // Log exception - // TODO(wd): Add logging - //s_log.Log(LogLevel.Error, () => $"Unable to create '{destinationPath}' file.", exc); - return Task.FromResult(new StoreItemResult(HttpStatusCode.InternalServerError)); - } - - // Return result - return Task.FromResult(new StoreItemResult(result, new EncryptingDiskStoreItem(LockingManager, new FileInfo(destinationPath), IsWritable, _specifics))); - } - - public Task CreateCollectionAsync(string name, bool overwrite, IHttpContext context) - { - // Return error - if (!IsWritable) - return Task.FromResult(new StoreCollectionResult(HttpStatusCode.Forbidden)); - - // Determine the destination path - var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(FullPath, name), _specifics); - - // Check if the directory can be overwritten - HttpStatusCode result; - if (Directory.Exists(destinationPath)) - { - // Check if overwrite is allowed - if (!overwrite) - return Task.FromResult(new StoreCollectionResult(HttpStatusCode.MethodNotAllowed)); - - // Overwrite existing - result = HttpStatusCode.NoContent; - } - else - { - // Created new directory - result = HttpStatusCode.Created; - } - - try - { - // Attempt to create the directory - Directory.CreateDirectory(destinationPath); - - // Create new DirectoryID - var directoryId = Guid.NewGuid().ToByteArray(); - var directoryIdPath = Path.Combine(destinationPath, FileSystem.Constants.Names.DIRECTORY_ID_FILENAME); - - // Initialize directory with DirectoryID - using var directoryIdStream = File.Open(directoryIdPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete); - directoryIdStream.Write(directoryId); - - // Set DirectoryID to known IDs - _specifics.DirectoryIdCache.CacheSet(directoryIdPath, new(directoryId)); - } - catch (Exception exc) - { - // Log exception - // TODO(wd): Add logging - //s_log.Log(LogLevel.Error, () => $"Unable to create '{destinationPath}' directory.", exc); - return null; - } - - // Return the collection - return Task.FromResult(new StoreCollectionResult(result, new EncryptingDiskStoreCollection(LockingManager, new DirectoryInfo(destinationPath), IsWritable, _specifics))); - } - - public async Task CopyAsync(IStoreCollection destinationCollection, string name, bool overwrite, IHttpContext context) - { - // Just create the folder itself - var result = await destinationCollection.CreateCollectionAsync(name, overwrite, context).ConfigureAwait(false); - return new StoreItemResult(result.Result, result.Collection); - } - - public bool SupportsFastMove(IStoreCollection destination, string destinationName, bool overwrite, IHttpContext context) - { - // We can only move disk-store collections - return destination is EncryptingDiskStoreCollection; - } - - public async Task MoveItemAsync(string sourceName, IStoreCollection destinationCollection, string destinationName, bool overwrite, IHttpContext context) - { - // Return error - if (!IsWritable) - return new StoreItemResult(HttpStatusCode.Forbidden); - - // Determine the object that is being moved - var item = await GetItemAsync(sourceName, context).ConfigureAwait(false); - if (item == null) - return new StoreItemResult(HttpStatusCode.NotFound); - - try - { - // If the destination collection is a directory too, then we can simply move the file - if (destinationCollection is EncryptingDiskStoreCollection destinationDiskStoreCollection) - { - // Return error - if (!destinationDiskStoreCollection.IsWritable) - return new StoreItemResult(HttpStatusCode.Forbidden); - - // Determine source and destination paths - var sourcePath = NativePathHelpers.GetCiphertextPath(Path.Combine(FullPath, sourceName), _specifics); - var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(destinationDiskStoreCollection.FullPath, destinationName), _specifics); - - // Check if the file already exists - HttpStatusCode result; - if (File.Exists(destinationPath)) - { - // Remove the file if it already exists (if allowed) - if (!overwrite) - return new StoreItemResult(HttpStatusCode.PreconditionFailed); - - // The file will be overwritten - File.Delete(destinationPath); - result = HttpStatusCode.NoContent; - } - else if (Directory.Exists(destinationPath)) - { - // Remove the directory if it already exists (if allowed) - if (!overwrite) - return new StoreItemResult(HttpStatusCode.PreconditionFailed); - - // The file will be overwritten - Directory.Delete(destinationPath, true); - result = HttpStatusCode.NoContent; - } - else - { - // The file will be "created" - result = HttpStatusCode.Created; - } - - switch (item) - { - case EncryptingDiskStoreItem _: - // Move the file - File.Move(sourcePath, destinationPath); - return new StoreItemResult(result, new EncryptingDiskStoreItem(LockingManager, new FileInfo(destinationPath), IsWritable, _specifics)); - - case EncryptingDiskStoreCollection _: - // Move the directory - Directory.Move(sourcePath, destinationPath); - return new StoreItemResult(result, new EncryptingDiskStoreCollection(LockingManager, new DirectoryInfo(destinationPath), IsWritable, _specifics)); - - default: - // Invalid item - Debug.Fail($"Invalid item {item.GetType()} inside the {nameof(DiskStoreCollection)}."); - return new StoreItemResult(HttpStatusCode.InternalServerError); - } - } - else - { - // Attempt to copy the item to the destination collection - var result = await item.CopyAsync(destinationCollection, destinationName, overwrite, context).ConfigureAwait(false); - if (result.Result == HttpStatusCode.Created || result.Result == HttpStatusCode.NoContent) - await DeleteItemAsync(sourceName, context).ConfigureAwait(false); - - // Return the result - return result; - } - } - catch (UnauthorizedAccessException) - { - return new StoreItemResult(HttpStatusCode.Forbidden); - } - } - - public Task DeleteItemAsync(string name, IHttpContext context) - { - // Return error - if (!IsWritable) - return Task.FromResult(HttpStatusCode.Forbidden); - - // Determine the full path - var fullPath = NativePathHelpers.GetCiphertextPath(Path.Combine(FullPath, name), _specifics); - try - { - // Check if the file exists - if (File.Exists(fullPath)) - { - // Delete the file - File.Delete(fullPath); - return Task.FromResult(HttpStatusCode.NoContent); - } - - // Check if the directory exists - if (Directory.Exists(fullPath)) - { - // Delete the directory - Directory.Delete(fullPath, true); - return Task.FromResult(HttpStatusCode.NoContent); - } - - // Item not found - return Task.FromResult(HttpStatusCode.NotFound); - } - catch (UnauthorizedAccessException) - { - return Task.FromResult(HttpStatusCode.Forbidden); - } - catch (Exception exc) - { - // Log exception - // TODO(wd): Add logging - //s_log.Log(LogLevel.Error, () => $"Unable to delete '{fullPath}' directory.", exc); - return Task.FromResult(HttpStatusCode.InternalServerError); - } - } - - public EnumerationDepthMode InfiniteDepthMode => EnumerationDepthMode.Rejected; - - public override int GetHashCode() - { - return _directoryInfo.FullName.GetHashCode(); - } - - public override bool Equals(object? obj) - { - if (obj is not EncryptingDiskStoreCollection storeCollection) - return false; - - return storeCollection._directoryInfo.FullName.Equals(_directoryInfo.FullName, StringComparison.CurrentCultureIgnoreCase); - } - } -} diff --git a/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs index 385ad7231..b9549558d 100644 --- a/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs +++ b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs @@ -1,34 +1,32 @@ -using Microsoft.Extensions.DependencyInjection; -using NWebDav.Server; -using NWebDav.Server.Stores; -using SecureFolderFS.Core.WebDav.AppModels; -using SecureFolderFS.Core.WebDav.EncryptingStorage2; -using System; +using System; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using NWebDav.Server.Stores; +using SecureFolderFS.Core.WebDav.AppModels; +using SecureFolderFS.Core.WebDav.Store; namespace SecureFolderFS.Core.WebDav.Extensions { internal static class BuilderExtensions { - public static IServiceCollection AddEncryptingDiskStore(this IServiceCollection services, Action options) + public static IServiceCollection AddCipherStore(this IServiceCollection services, Action options) { return services .Configure(options) .AddSingleton() .AddSingleton() - .AddScoped(sp => + .AddScoped(sp => { - var storeOptions = sp.GetService>(); + var storeOptions = sp.GetService>(); if (storeOptions?.Value.Specifics is null) throw new NotSupportedException("Options were not configured."); var itemPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreItemPropertyManager)); var collectionPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreCollectionPropertyManager)); - var loggerFactory = sp.GetService() ?? throw new ArgumentNullException(nameof(ILoggerFactory)); - - return new(storeOptions.Value.Specifics, itemPropertyManager, collectionPropertyManager, loggerFactory); + var logger = sp.GetService() ?? throw new ArgumentNullException(nameof(ILogger)); + return new(storeOptions.Value.Specifics, itemPropertyManager, collectionPropertyManager, logger); }); } } diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStore.cs similarity index 64% rename from src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs rename to src/SecureFolderFS.Core.WebDav/Store/CipherStore.cs index c092377a6..ac93321ed 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStore.cs @@ -1,14 +1,14 @@ -using Microsoft.Extensions.Logging; +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using NWebDav.Server.Props; using NWebDav.Server.Stores; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Core.FileSystem.Helpers.Native; -using SecureFolderFS.Core.WebDav.Base; -using System; -using System.IO; -namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 +namespace SecureFolderFS.Core.WebDav.Store { - internal sealed class EncryptingDiskStore : DiskStoreBase2 + internal sealed class CipherStore : CipherStoreBase { private readonly FileSystemSpecifics _specifics; @@ -18,12 +18,12 @@ internal sealed class EncryptingDiskStore : DiskStoreBase2 /// public override string BaseDirectory { get; } - public EncryptingDiskStore( + public CipherStore( FileSystemSpecifics specifics, - DiskStoreItemPropertyManager itemPropertyManager, - DiskStoreCollectionPropertyManager collectionPropertyManager, - ILoggerFactory loggerFactory) - : base(collectionPropertyManager, itemPropertyManager, loggerFactory) + IPropertyManager itemPropertyManager, + IPropertyManager collectionPropertyManager, + ILogger logger) + : base(collectionPropertyManager, itemPropertyManager, logger) { _specifics = specifics; IsWritable = !specifics.FileSystemOptions.IsReadOnly; @@ -35,18 +35,18 @@ public EncryptingDiskStore( { if (typeof(T).IsAssignableFrom(typeof(IStoreCollection))) { - return (T?)(object)new EncryptingDiskStoreCollection(new DirectoryInfo(path)); + return (T?)(object)new CipherStoreCollection(new(path), collectionPropertyManager, _specifics, this, logger); } - //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add a StoreFile + //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add an IStoreFile else { - // Check if it's a directory - if (Directory.Exists(path)) - return (T?)(object)new EncryptingDiskStoreCollection(new DirectoryInfo(path)); - // Check if it's a file if (File.Exists(path)) - return (T?)(object)new EncryptingDiskStoreItem(new FileInfo(path)); + return (T?)(object)new CipherStoreItem(new(path), itemPropertyManager, _specifics, this, logger); + + // Check if it's a directory + if (Directory.Exists(path)) + return (T?)(object)new CipherStoreCollection(new(path), collectionPropertyManager, _specifics, this, logger); // The item doesn't exist return null; diff --git a/src/SecureFolderFS.Core.WebDav/Store/CipherStoreBase.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreBase.cs new file mode 100644 index 000000000..4a77f20ff --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreBase.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using NWebDav.Server.Stores; +using System; +using System.IO; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using NWebDav.Server.Helpers; +using NWebDav.Server.Props; + +namespace SecureFolderFS.Core.WebDav.Store +{ + internal abstract class CipherStoreBase : IStore + { + protected readonly IPropertyManager collectionPropertyManager; + protected readonly IPropertyManager itemPropertyManager; + protected readonly ILogger logger; + + public abstract bool IsWritable { get; } + + public abstract string BaseDirectory { get; } + + protected CipherStoreBase(IPropertyManager collectionPropertyManager, IPropertyManager itemPropertyManager, ILogger logger) + { + this.collectionPropertyManager = collectionPropertyManager; + this.itemPropertyManager = itemPropertyManager; + this.logger = logger; + } + + public virtual async Task GetItemAsync(Uri uri, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Get path and item + var path = GetPathFromUri(uri); + var item = CreateFromPath(path); + + return item; + } + + public virtual async Task GetCollectionAsync(Uri uri, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Determine the path from the uri + var path = GetPathFromUri(uri); + if (!Directory.Exists(path)) + return null; + + // Return the item + return CreateFromPath(path); + } + + protected virtual string GetPathFromUri(Uri uri) + { + // Determine the path + var requestedPath = UriHelper.GetDecodedPath(uri)[1..].Replace('/', Path.DirectorySeparatorChar); + + // Determine the full path + var fullPath = Path.GetFullPath(Path.Combine(BaseDirectory, requestedPath)); + + // Make sure we're still inside the specified directory + if (fullPath != BaseDirectory && !fullPath.StartsWith(BaseDirectory + Path.DirectorySeparatorChar)) + throw new SecurityException($"Uri '{uri}' is outside the '{BaseDirectory}' directory."); + + // Return the combined path + return fullPath; + } + + public abstract T? CreateFromPath(string path) where T : class, IStoreItem; + } +} diff --git a/src/SecureFolderFS.Core.WebDav/Store/CipherStoreCollection.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreCollection.cs new file mode 100644 index 000000000..c852aa482 --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreCollection.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NWebDav.Server; +using NWebDav.Server.Props; +using NWebDav.Server.Stores; +using SecureFolderFS.Core.FileSystem; +using SecureFolderFS.Core.FileSystem.Helpers; +using SecureFolderFS.Core.FileSystem.Helpers.Native; + +namespace SecureFolderFS.Core.WebDav.Store +{ + internal sealed class CipherStoreCollection : IStoreCollection + { + private readonly FileSystemSpecifics _specifics; + private readonly CipherStore _store; + private readonly ILogger _logger; + + /// + public string Name { get; } + + /// + public string UniqueKey { get; } + + /// + public InfiniteDepthMode InfiniteDepthMode { get; } = InfiniteDepthMode.Rejected; + + /// + public IPropertyManager PropertyManager { get; } + + public DirectoryInfo DirectoryInfo { get; } // TODO: Not from interface + public bool IsWritable { get; } // TODO: Not from interface + + public CipherStoreCollection(DirectoryInfo directoryInfo, IPropertyManager propertyManager, FileSystemSpecifics specifics, CipherStore store, ILogger logger) + { + _specifics = specifics; + _store = store; + _logger = logger; + DirectoryInfo = directoryInfo; + IsWritable = specifics.FileSystemOptions.IsReadOnly; + UniqueKey = NativePathHelpers.GetPlaintextPath(directoryInfo.FullName, specifics) ?? string.Empty; + Name = Path.GetFileName(UniqueKey); + DirectoryInfo = directoryInfo; + PropertyManager = propertyManager; + } + + /// + public Task GetReadableStreamAsync(CancellationToken cancellationToken) + => Task.FromResult(Stream.Null); + + /// + public Task UploadFromStreamAsync(Stream inputStream, CancellationToken cancellationToken) + => Task.FromResult(DavStatusCode.Conflict); + + /// + public async Task GetItemAsync(string name, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Get the item path + var fullPath = NativePathHelpers.GetCiphertextPath(Path.Combine(UniqueKey, name), _specifics); + + // Create a new item instance + return _store.CreateFromPath(fullPath); + } + + /// + public async IAsyncEnumerable GetItemsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Add all directories + foreach (var item in Directory.EnumerateDirectories(DirectoryInfo.FullName)) + { + if (PathHelpers.IsCoreFile(Path.GetFileName(item))) + continue; + + var directory = _store.CreateFromPath(item); + if (directory is null) + continue; + + yield return directory; + } + + // Add all files + foreach (var item in Directory.EnumerateFiles(DirectoryInfo.FullName)) + { + if (PathHelpers.IsCoreFile(Path.GetFileName(item))) + continue; + + var file = _store.CreateFromPath(item); + if (file is null) + continue; + + yield return file; + } + } + + /// + public async Task CreateItemAsync(string name, Stream stream, bool overwrite, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Return error + if (!IsWritable) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + // Determine the destination path + var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(UniqueKey, name), _specifics); + + // Check if the file can be overwritten + if (File.Exists(destinationPath) && !overwrite) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + try + { + var file = File.Create(destinationPath); + await using (file.ConfigureAwait(false)) + { + await stream.CopyToAsync(file, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception exc) + { + // Log exception + _logger.LogError(exc, "Unable to create '{Path}' file.", destinationPath); + return new StoreItemResult(DavStatusCode.InternalServerError); + } + + // Return result + var item = _store.CreateFromPath(destinationPath); + return new StoreItemResult(DavStatusCode.Created, item); + } + + /// + public async Task CreateCollectionAsync(string name, bool overwrite, CancellationToken cancellationToken) + { + await Task.CompletedTask; + cancellationToken.ThrowIfCancellationRequested(); + + // Return error + if (!IsWritable) + return new StoreCollectionResult(DavStatusCode.PreconditionFailed); + + // Determine the destination path + var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(DirectoryInfo.FullName, name), _specifics); + + // Check if the directory can be overwritten + DavStatusCode result; + if (Directory.Exists(destinationPath)) + { + // Check if overwrite is allowed + if (!overwrite) + return new StoreCollectionResult(DavStatusCode.PreconditionFailed); + + // Overwrite existing + result = DavStatusCode.NoContent; + } + else + { + // Created new directory + result = DavStatusCode.Created; + } + + // Attempt to create the directory + Directory.CreateDirectory(destinationPath); + + // Return the collection + return new StoreCollectionResult(result, _store.CreateFromPath(destinationPath)); + } + + /// + public async Task CopyAsync(IStoreCollection destinationCollection, string name, bool overwrite, CancellationToken cancellationToken) + { + // Just create the folder itself + var result = await destinationCollection.CreateCollectionAsync(name, overwrite, cancellationToken).ConfigureAwait(false); + return new StoreItemResult(result.Result, result.Collection); + } + + /// + public bool SupportsFastMove(IStoreCollection destination, string destinationName, bool overwrite) + { + // We can only move disk-store collections + return destination is CipherStoreCollection; + } + + /// + public async Task MoveItemAsync(string sourceName, IStoreCollection destinationCollection, string destinationName, bool overwrite, CancellationToken cancellationToken) + { + // Return error + if (!IsWritable) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + // Determine the object that is being moved + var item = await GetItemAsync(sourceName, cancellationToken).ConfigureAwait(false); + if (item == null) + return new StoreItemResult(DavStatusCode.NotFound); + + try + { + // If the destination collection is a directory too, then we can simply move the file + if (destinationCollection is DiskStoreCollection destinationDiskStoreCollection) + { + // Return error + if (!destinationDiskStoreCollection.IsWritable) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + // Determine source and destination paths + var sourcePath = NativePathHelpers.GetCiphertextPath(Path.Combine(DirectoryInfo.FullName, sourceName), _specifics); + var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(destinationDiskStoreCollection.DirectoryInfo.FullName, destinationName), _specifics); + + // Check if the file already exists + DavStatusCode result; + if (File.Exists(destinationPath)) + { + // Remove the file if it already exists (if allowed) + if (!overwrite) + return new StoreItemResult(DavStatusCode.Forbidden); + + // The file will be overwritten + File.Delete(destinationPath); + result = DavStatusCode.NoContent; + } + else if (Directory.Exists(destinationPath)) + { + // Remove the directory if it already exists (if allowed) + if (!overwrite) + return new StoreItemResult(DavStatusCode.Forbidden); + + // The file will be overwritten + Directory.Delete(destinationPath, true); + result = DavStatusCode.NoContent; + } + else + { + // The file will be "created" + result = DavStatusCode.Created; + } + + switch (item) + { + case DiskStoreItem _: + // Move the file + File.Move(sourcePath, destinationPath); + return new StoreItemResult(result, _store.CreateFromPath(destinationPath)); + + case DiskStoreCollection _: + // Move the directory + Directory.Move(sourcePath, destinationPath); + return new StoreItemResult(result, _store.CreateFromPath(destinationPath)); + + default: + // Invalid item + Debug.Fail($"Invalid item {item.GetType()} inside the {nameof(DiskStoreCollection)}."); + return new StoreItemResult(DavStatusCode.InternalServerError); + } + } + else + { + // Attempt to copy the item to the destination collection + var result = await item.CopyAsync(destinationCollection, destinationName, overwrite, cancellationToken).ConfigureAwait(false); + if (result.Result == DavStatusCode.Created || result.Result == DavStatusCode.NoContent) + await DeleteItemAsync(sourceName, cancellationToken).ConfigureAwait(false); + + // Return the result + return result; + } + } + catch (UnauthorizedAccessException) + { + return new StoreItemResult(DavStatusCode.Forbidden); + } + } + + /// + public async Task DeleteItemAsync(string name, CancellationToken cancellationToken) + { + await Task.CompletedTask; + + // Return error + if (!IsWritable) + return DavStatusCode.PreconditionFailed; + + // Determine the full path + var fullPath = NativePathHelpers.GetCiphertextPath(Path.Combine(DirectoryInfo.FullName, name), _specifics); + try + { + // Check if the file exists + if (File.Exists(fullPath)) + { + // Delete the file + File.Delete(fullPath); + return DavStatusCode.Ok; + } + + // Check if the directory exists + if (Directory.Exists(fullPath)) + { + // Delete the directory + Directory.Delete(fullPath, true); + return DavStatusCode.Ok; + } + + // Item not found + return DavStatusCode.NotFound; + } + catch (UnauthorizedAccessException) + { + return DavStatusCode.Forbidden; + } + catch (Exception exc) + { + // Log exception + _logger.LogError(exc, "Unable to delete '{Path}' directory.", fullPath); + return DavStatusCode.InternalServerError; + } + } + } +} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreItem.cs similarity index 89% rename from src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs rename to src/SecureFolderFS.Core.WebDav/Store/CipherStoreItem.cs index 487e62917..7c6b06406 100644 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreItem.cs @@ -10,12 +10,13 @@ using System.Threading; using System.Threading.Tasks; -namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 +namespace SecureFolderFS.Core.WebDav.Store { - internal class EncryptingDiskStoreItem : IStoreItem + internal class CipherStoreItem : IStoreItem { private readonly FileSystemSpecifics _specifics; - private readonly ILogger _logger; + private readonly CipherStore _store; + private readonly ILogger _logger; /// public string Name { get; } @@ -29,10 +30,11 @@ internal class EncryptingDiskStoreItem : IStoreItem public FileInfo FileInfo { get; } // TODO: Not from interface public bool IsWritable { get; } // TODO: Not from interface - public EncryptingDiskStoreItem(FileInfo fileInfo, DiskStoreItemPropertyManager propertyManager, FileSystemSpecifics specifics, ILogger logger) + public CipherStoreItem(FileInfo fileInfo, IPropertyManager propertyManager, FileSystemSpecifics specifics, CipherStore store, ILogger logger) { - _logger = logger; _specifics = specifics; + _store = store; + _logger = logger; IsWritable = !specifics.FileSystemOptions.IsReadOnly; UniqueKey = NativePathHelpers.GetPlaintextPath(fileInfo.FullName, specifics) ?? string.Empty; Name = Path.GetFileName(UniqueKey); @@ -79,13 +81,13 @@ public async Task CopyAsync(IStoreCollection destination, strin { // If the destination is also a disk-store, then we can use the FileCopy API // (it's probably a bit more efficient than copying in C#) - if (destination is EncryptingDiskStoreCollection diskCollection) + if (destination is CipherStoreCollection diskCollection) { // Check if the collection is writable if (!diskCollection.IsWritable) return new StoreItemResult(DavStatusCode.PreconditionFailed); - var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(diskCollection.FullPath, name), _specifics); + var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(diskCollection.UniqueKey, name), _specifics); // Check if the file already exists var fileExists = File.Exists(destinationPath); diff --git a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs index 6034d55fc..ddec336bc 100644 --- a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs +++ b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs @@ -1,21 +1,18 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using NWebDav.Server; using OwlCore.Storage; using SecureFolderFS.Core.Cryptography; using SecureFolderFS.Core.FileSystem; using SecureFolderFS.Core.WebDav.AppModels; -using SecureFolderFS.Core.WebDav.EncryptingStorage2; using SecureFolderFS.Core.WebDav.Extensions; using SecureFolderFS.Core.WebDav.Helpers; using SecureFolderFS.Shared.ComponentModel; using SecureFolderFS.Storage.Enums; using SecureFolderFS.Storage.VirtualFileSystem; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading; -using System.Threading.Tasks; namespace SecureFolderFS.Core.WebDav { @@ -52,7 +49,7 @@ public virtual async Task MountAsync(IFolder folder, IDisposable unloc var builder = WebApplication.CreateBuilder(); builder.Services .AddNWebDav() - .AddEncryptingDiskStore(x => + .AddCipherStore(x => { x.Specifics = specifics; }); @@ -65,11 +62,6 @@ public virtual async Task MountAsync(IFolder folder, IDisposable unloc webDavOptions, webDavInstance, cancellationToken); - - - // TODO: Remove the following line once the new DavStorage is fully implemented. - var encryptingDiskStore = new EncryptingDiskStore(specifics.ContentFolder.Id, specifics, !specifics.FileSystemOptions.IsReadOnly); - var dispatcher = new WebDavDispatcher(new RootDiskStore(specifics.FileSystemOptions.VolumeName, encryptingDiskStore), davFolder, new RequestHandlerProvider(), null); } protected abstract Task MountAsync( diff --git a/src/SecureFolderFS.Core.WebDav/WebDavWrapper.cs b/src/SecureFolderFS.Core.WebDav/WebDavWrapper.cs deleted file mode 100644 index db6db58b2..000000000 --- a/src/SecureFolderFS.Core.WebDav/WebDavWrapper.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NWebDav.Server.Dispatching; -using NWebDav.Server.HttpListener; -using SecureFolderFS.Core.WebDav.Helpers; -using System; -using System.Diagnostics; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace SecureFolderFS.Core.WebDav -{ - public sealed class WebDavWrapper - { - private Thread? _fsThead; - private readonly HttpListener _httpListener; - private readonly IRequestDispatcher _requestDispatcher; - private readonly CancellationTokenSource _fileSystemCts; - private readonly string? _mountPath; - - public WebDavWrapper(HttpListener httpListener, IRequestDispatcher requestDispatcher, string? mountPath = null) - { - _httpListener = httpListener; - _requestDispatcher = requestDispatcher; - _fileSystemCts = new(); - _mountPath = mountPath; - } - - public void StartFileSystem() - { - var ts = new ThreadStart(async () => await EnsureFileSystemAsync()); - _fsThead = new Thread(ts); - _fsThead.Start(); - } - - private async Task EnsureFileSystemAsync() - { - try - { - _httpListener.Start(); - while (!_fileSystemCts.IsCancellationRequested && (await _httpListener.GetContextAsync() is var httpListenerContext)) - { - if (httpListenerContext.Request.IsAuthenticated) - Debugger.Break(); - - var context = new HttpContext(httpListenerContext); - await _requestDispatcher.DispatchRequestAsync(context, _fileSystemCts.Token); - } - } - catch (Exception ex) - { - _ = ex; - } - } - - public async Task CloseFileSystemAsync() - { - _httpListener.Close(); - await _fileSystemCts.CancelAsync(); - - if (_mountPath is not null) - DriveMappingHelpers.DisconnectNetworkDrive(_mountPath, true); - - return true; - } - } -}