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/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/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs b/src/Platforms/SecureFolderFS.Uno/Platforms/Windows/WindowsWebDavFileSystem.cs index 618673ba0..d55c2c68b 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,12 +16,11 @@ 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); + var remotePath = DriveMappingHelpers.GetRemotePath(options.Protocol, options.Domain, options.Port, options.VolumeName); var mountPath = await DriveMappingHelpers.GetMountPathForRemotePathAsync(remotePath); if (mountPath is null) { @@ -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/CipherStoreOptions.cs b/src/SecureFolderFS.Core.WebDav/AppModels/CipherStoreOptions.cs new file mode 100644 index 000000000..678f41813 --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/AppModels/CipherStoreOptions.cs @@ -0,0 +1,9 @@ +using SecureFolderFS.Core.FileSystem; + +namespace SecureFolderFS.Core.WebDav.AppModels +{ + internal sealed class CipherStoreOptions + { + public FileSystemSpecifics? Specifics { get; set; } + } +} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs deleted file mode 100644 index a5b0741ba..000000000 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStore.cs +++ /dev/null @@ -1,56 +0,0 @@ -using NWebDav.Server.Http; -using NWebDav.Server.Locking; -using NWebDav.Server.Stores; -using SecureFolderFS.Core.FileSystem; -using SecureFolderFS.Core.FileSystem.Helpers.Native; -using System; -using System.IO; -using System.Threading.Tasks; - -namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 -{ - internal sealed class EncryptingDiskStore : DiskStore - { - private readonly FileSystemSpecifics _specifics; - - public EncryptingDiskStore(string directory, FileSystemSpecifics specifics, bool isWritable = true, ILockingManager? lockingManager = null) - : base(directory, isWritable, lockingManager) - { - _specifics = specifics; - } - - public override Task GetItemAsync(Uri uri, IHttpContext context) - { - // 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)); - } - - protected override string GetPathFromUri(Uri uri) - { - var path = base.GetPathFromUri(uri); - return NativePathHelpers.GetCiphertextPath(path, _specifics); - } - } -} diff --git a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreCollection.cs deleted file mode 100644 index 75530723f..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 : IDiskStoreCollection - { - 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/EncryptingStorage2/EncryptingDiskStoreItem.cs b/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs deleted file mode 100644 index d3d7ae0d1..000000000 --- a/src/SecureFolderFS.Core.WebDav/EncryptingStorage2/EncryptingDiskStoreItem.cs +++ /dev/null @@ -1,229 +0,0 @@ -using NWebDav.Server.Helpers; -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.Native; -using System; -using System.IO; -using System.Net; -using System.Threading.Tasks; - -namespace SecureFolderFS.Core.WebDav.EncryptingStorage2 -{ - internal class EncryptingDiskStoreItem : IDiskStoreItem - { - private readonly FileSystemSpecifics _specifics; - private readonly FileInfo _fileInfo; - - public EncryptingDiskStoreItem(ILockingManager lockingManager, FileInfo fileInfo, bool isWritable, FileSystemSpecifics specifics) - { - LockingManager = lockingManager; - IsWritable = isWritable; - _fileInfo = fileInfo; - _specifics = specifics; - } - - 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 - }, - - // Default locking property handling via the LockingManager - new DavLockDiscoveryDefault(), - new DavSupportedLockDefault(), - - // 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 - }, - - // 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 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 UploadFromStreamAsync(IHttpContext context, Stream inputStream) - { - // Check if the item is writable - if (!IsWritable) - return HttpStatusCode.Forbidden; - - // Copy the stream - try - { - // Copy the information to the destination stream - using (var outputStream = await GetWritableStreamAsync(context).ConfigureAwait(false)) - { - await inputStream.CopyToAsync(outputStream).ConfigureAwait(false); - } - - return HttpStatusCode.OK; - } - catch (IOException ioException) when (ioException.IsDiskFull()) - { - return HttpStatusCode.InsufficientStorage; - } - catch (Exception ex) - { - _ = ex; - throw; - } - } - - public IPropertyManager PropertyManager => DefaultPropertyManager; - public ILockingManager LockingManager { get; } - - public async Task CopyAsync(IStoreCollection destination, string name, bool overwrite, IHttpContext context) - { - 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) - { - // Check if the collection is writable - if (!diskCollection.IsWritable) - return new StoreItemResult(HttpStatusCode.Forbidden); - - 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); - - // Copy the file - File.Copy(_fileInfo.FullName, destinationPath, true); - - // Return the appropriate status - return new StoreItemResult(fileExists ? HttpStatusCode.NoContent : HttpStatusCode.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) - { - 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 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); - } - } - - 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/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..b9549558d --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Extensions/BuilderExtensions.cs @@ -0,0 +1,33 @@ +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 AddCipherStore(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."); + + var itemPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreItemPropertyManager)); + var collectionPropertyManager = sp.GetService() ?? throw new ArgumentNullException(nameof(DiskStoreCollectionPropertyManager)); + var logger = sp.GetService() ?? throw new ArgumentNullException(nameof(ILogger)); + + return new(storeOptions.Value.Specifics, itemPropertyManager, collectionPropertyManager, logger); + }); + } + } +} 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/Store/CipherStore.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStore.cs new file mode 100644 index 000000000..ac93321ed --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStore.cs @@ -0,0 +1,63 @@ +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; + +namespace SecureFolderFS.Core.WebDav.Store +{ + internal sealed class CipherStore : CipherStoreBase + { + private readonly FileSystemSpecifics _specifics; + + /// + public override bool IsWritable { get; } + + /// + public override string BaseDirectory { get; } + + public CipherStore( + FileSystemSpecifics specifics, + IPropertyManager itemPropertyManager, + IPropertyManager collectionPropertyManager, + ILogger logger) + : base(collectionPropertyManager, itemPropertyManager, logger) + { + _specifics = specifics; + IsWritable = !specifics.FileSystemOptions.IsReadOnly; + BaseDirectory = specifics.ContentFolder.Id; + } + + public override T? CreateFromPath(string path) + where T : class + { + if (typeof(T).IsAssignableFrom(typeof(IStoreCollection))) + { + return (T?)(object)new CipherStoreCollection(new(path), collectionPropertyManager, _specifics, this, logger); + } + //else if (typeof(T).IsAssignableFrom(typeof(IStoreFile))) // TODO: Add an IStoreFile + else + { + // Check if it's a file + if (File.Exists(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; + } + } + + /// + protected override string GetPathFromUri(Uri uri) + { + var path = base.GetPathFromUri(uri); + return NativePathHelpers.GetCiphertextPath(path, _specifics); + } + } +} 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/Store/CipherStoreItem.cs b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreItem.cs new file mode 100644 index 000000000..7c6b06406 --- /dev/null +++ b/src/SecureFolderFS.Core.WebDav/Store/CipherStoreItem.cs @@ -0,0 +1,120 @@ +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.Threading; +using System.Threading.Tasks; + +namespace SecureFolderFS.Core.WebDav.Store +{ + internal class CipherStoreItem : IStoreItem + { + private readonly FileSystemSpecifics _specifics; + private readonly CipherStore _store; + private readonly ILogger _logger; + + /// + public string Name { get; } + + /// + public string UniqueKey { get; } + + /// + public IPropertyManager PropertyManager { get; } + + public FileInfo FileInfo { get; } // TODO: Not from interface + public bool IsWritable { get; } // TODO: Not from interface + + public CipherStoreItem(FileInfo fileInfo, IPropertyManager propertyManager, FileSystemSpecifics specifics, CipherStore store, ILogger logger) + { + _specifics = specifics; + _store = store; + _logger = logger; + IsWritable = !specifics.FileSystemOptions.IsReadOnly; + UniqueKey = NativePathHelpers.GetPlaintextPath(fileInfo.FullName, specifics) ?? string.Empty; + Name = Path.GetFileName(UniqueKey); + FileInfo = fileInfo; + PropertyManager = propertyManager; + } + + /// + public async Task GetReadableStreamAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + return _specifics.StreamsAccess.OpenPlaintextStream(FileInfo.FullName, FileInfo.OpenRead()); + } + + /// + public async Task UploadFromStreamAsync(Stream inputStream, CancellationToken cancellationToken) + { + // Check if the item is writable + if (!IsWritable) + return DavStatusCode.Conflict; + + try + { + // Copy the information to the destination stream + var outputStream = _specifics.StreamsAccess.OpenPlaintextStream(FileInfo.FullName, FileInfo.OpenWrite()); + await using (outputStream.ConfigureAwait(false)) + { + // Copy the stream + await inputStream.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + + return DavStatusCode.Ok; + } + catch (IOException ioException) when (ioException.IsDiskFull()) + { + return DavStatusCode.InsufficientStorage; + } + } + + /// + 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 CipherStoreCollection diskCollection) + { + // Check if the collection is writable + if (!diskCollection.IsWritable) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + var destinationPath = NativePathHelpers.GetCiphertextPath(Path.Combine(diskCollection.UniqueKey, name), _specifics); + + // Check if the file already exists + var fileExists = File.Exists(destinationPath); + if (fileExists && !overwrite) + return new StoreItemResult(DavStatusCode.PreconditionFailed); + + // Copy the file + File.Copy(FileInfo.FullName, destinationPath, true); + + // Return the appropriate status + return new StoreItemResult(fileExists ? DavStatusCode.NoContent : DavStatusCode.Created); + } + else + { + // Create the item in the destination collection + var sourceStream = await GetReadableStreamAsync(cancellationToken).ConfigureAwait(false); + await using (sourceStream.ConfigureAwait(false)) + { + return await destination.CreateItemAsync(name, sourceStream, overwrite, cancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception exc) + { + _logger.LogError(exc, "Unexpected exception while copying data."); + return new StoreItemResult(DavStatusCode.InternalServerError); + } + } + } +} diff --git a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs index 8ca438d28..ddec336bc 100644 --- a/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs +++ b/src/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs @@ -1,21 +1,18 @@ -using NWebDav.Server; -using NWebDav.Server.Dispatching; -using NWebDav.Server.Storage; -using NWebDav.Server.Stores; +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 { @@ -48,31 +45,28 @@ 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 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 url = $"{webDavOptions.Protocol}://{webDavOptions.Domain}:{webDavOptions.Port}/"; + var builder = WebApplication.CreateBuilder(); + builder.Services + .AddNWebDav() + .AddCipherStore(x => + { + x.Specifics = specifics; + }); - // 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); + var webDavInstance = builder.Build(); + webDavInstance.UseNWebDav(); + _ = webDavInstance.RunAsync(url); return await MountAsync( - httpListener, webDavOptions, - dispatcher, + webDavInstance, 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); } } } 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; - } - } -}