diff --git a/README.md b/README.md index 573a7821..a809245d 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,13 @@ The library is released under the [![MIT license](https://img.shields.io/github/ ## Compilation -* Visual Studio 2017 / C# 7.3 +* Visual Studio 2019 / C# 8.0 ## Using -* Visual Studio 2017 -* .NET Standard 2.0 +* Visual Studio 2019 +* .NET Standard 2.0 (everything **except** sample application, PAM authentication) +* .NET Core 3.0 (sample application, PAM authentication) ## NuGet packages diff --git a/samples/TestFtpServer/Program.cs b/samples/TestFtpServer/Program.cs index a9dcf8d0..1ea9c31c 100644 --- a/samples/TestFtpServer/Program.cs +++ b/samples/TestFtpServer/Program.cs @@ -17,6 +17,7 @@ using FubarDev.FtpServer.FileSystem.DotNet; using FubarDev.FtpServer.FileSystem.GoogleDrive; using FubarDev.FtpServer.FileSystem.InMemory; +using FubarDev.FtpServer.MembershipProvider.Pam; using Google.Apis.Auth.OAuth2; using Google.Apis.Drive.v3; @@ -47,16 +48,21 @@ private static int Main(string[] args) switch (v) { case "custom": - options.MembershipProviderType = MembershipProviderType.Custom; + options.MembershipProviderType |= MembershipProviderType.Custom; break; case "anonymous": - options.MembershipProviderType = MembershipProviderType.Anonymous; + options.MembershipProviderType |= MembershipProviderType.Anonymous; + break; + case "pam": + options.MembershipProviderType |= MembershipProviderType.PAM; break; default: throw new ApplicationException("Invalid authentication module"); } } }, + "Workarounds", + { "no-pam-account-management", "Disable the PAM account management", v => options.NoPamAccountManagement = v != null }, "Server", { "a|address=", "Sets the IP address or host name", v => options.ServerAddress = v }, { "p|port=", "Sets the listen port", v => options.Port = Convert.ToInt32(v) }, @@ -92,6 +98,14 @@ private static int Main(string[] args) }, Run = a => RunWithFileSystemAsync(a.ToArray(), options).Wait(), }, + new Command("unix", "Use the Unix file system access") + { + Options = new OptionSet() + { + "usage: ftpserver unix", + }, + Run = a => RunWithUnixFileSystemAsync(options).Wait(), + }, new Command("in-memory", "Use the in-memory file system access") { Options = new OptionSet() @@ -148,6 +162,14 @@ private static Task RunWithFileSystemAsync(string[] args, TestFtpServerOptions o return RunAsync(services); } + private static Task RunWithUnixFileSystemAsync(TestFtpServerOptions options) + { + options.Validate(); + var services = CreateServices(options) + .AddFtpServer(sb => Configure(sb, options).UseUnixFileSystem()); + return RunAsync(services); + } + private static async Task RunWithGoogleDriveUserAsync(string[] args, TestFtpServerOptions options) { options.Validate(); @@ -255,7 +277,8 @@ private static IServiceCollection CreateServices(TestFtpServerOptions options) opt.PasvMaxPort = options.PassivePortRange.Value.Item2; } }) - .Configure(opt => opt.UseBackgroundUpload = options.UseBackgroundUpload); + .Configure(opt => opt.UseBackgroundUpload = options.UseBackgroundUpload) + .Configure(opt => opt.IgnoreAccountManagement = options.NoPamAccountManagement); if (options.ImplicitFtps) { @@ -286,15 +309,24 @@ private static IServiceCollection CreateServices(TestFtpServerOptions options) private static IFtpServerBuilder Configure(IFtpServerBuilder builder, TestFtpServerOptions options) { - switch (options.MembershipProviderType) + if (options.MembershipProviderType == MembershipProviderType.Default) + { + return builder.EnableAnonymousAuthentication(); + } + + if ((options.MembershipProviderType & MembershipProviderType.Anonymous) != 0) + { + builder = builder.EnableAnonymousAuthentication(); + } + + if ((options.MembershipProviderType & MembershipProviderType.Custom) != 0) + { + builder.Services.AddSingleton(); + } + + if ((options.MembershipProviderType & MembershipProviderType.PAM) != 0) { - case MembershipProviderType.Anonymous: - return builder.EnableAnonymousAuthentication(); - case MembershipProviderType.Custom: - builder.Services.AddSingleton(); - break; - default: - throw new InvalidOperationException($"Unknown membership provider {options.MembershipProviderType}"); + builder = builder.EnablePamAuthentication(); } return builder; diff --git a/samples/TestFtpServer/TestFtpServerOptions.cs b/samples/TestFtpServer/TestFtpServerOptions.cs index 31d68a6e..8fc85db3 100644 --- a/samples/TestFtpServer/TestFtpServerOptions.cs +++ b/samples/TestFtpServer/TestFtpServerOptions.cs @@ -14,7 +14,7 @@ public class TestFtpServerOptions /// /// Gets or sets a value indicating whether the help message should be shown. /// - public bool ShowHelp { get;set; } + public bool ShowHelp { get; set; } /// /// Gets or sets the requested server address. @@ -59,13 +59,18 @@ public class TestFtpServerOptions /// /// Gets or sets the membership provider to be used. /// - public MembershipProviderType MembershipProviderType { get; set; } = MembershipProviderType.Anonymous; + public MembershipProviderType MembershipProviderType { get; set; } = MembershipProviderType.Default; /// /// Gets or sets the passive port range. /// public (int, int)? PassivePortRange { get; set; } + /// + /// Gets or sets a value indicating whether PAM account management is disabled. + /// + public bool NoPamAccountManagement { get; set; } + /// /// Gets the requested or the default port. /// @@ -90,16 +95,27 @@ public void Validate() /// /// The selected membership provider. /// + [Flags] public enum MembershipProviderType { + /// + /// Use the default membership provider (). + /// + Default = 0, + /// /// Use the custom (example) membership provider. /// - Custom, + Custom = 1, /// /// Use the membership provider for anonymous users. /// - Anonymous, + Anonymous = 2, + + /// + /// Use the PAM membership provider. + /// + PAM = 4, } } diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixDirectoryEntry.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixDirectoryEntry.cs index ac932fec..c348a93c 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixDirectoryEntry.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixDirectoryEntry.cs @@ -22,6 +22,7 @@ public UnixDirectoryEntry( : base(info) { IsRoot = parent == null; + Info = info; if (parent == null) { @@ -35,10 +36,15 @@ public UnixDirectoryEntry( } else { - IsDeletable = parent.GetEffectivePermissions(user, userInfo).Write; + IsDeletable = parent.GetEffectivePermissions(user, userInfo.UserId, userInfo.GroupId).Write; } } + /// + /// Gets the unix directory info. + /// + public UnixDirectoryInfo Info { get; } + /// public bool IsRoot { get; } diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileEntry.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileEntry.cs index d7e28ddf..1303ccf2 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileEntry.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileEntry.cs @@ -17,9 +17,15 @@ internal class UnixFileEntry : UnixFileSystemEntry, IUnixFileEntry public UnixFileEntry([NotNull] UnixFileInfo info) : base(info) { + Info = info; Size = info.Length; } + /// + /// Gets the unix file info. + /// + public UnixFileInfo Info { get; } + /// public long Size { get; } } diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystem.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystem.cs index 22b19262..84a19cc5 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystem.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystem.cs @@ -5,23 +5,32 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using FubarDev.FtpServer.AccountManagement; using FubarDev.FtpServer.BackgroundTransfer; +using Mono.Unix; +using Mono.Unix.Native; + namespace FubarDev.FtpServer.FileSystem.Unix { internal class UnixFileSystem : IUnixFileSystem { private readonly IFtpUser _user; + private readonly UnixUserInfo _userInfo; + public UnixFileSystem( IUnixDirectoryEntry root, - IFtpUser user) + IFtpUser user, + UnixUserInfo userInfo) { _user = user; + _userInfo = userInfo; Root = root; } @@ -40,13 +49,21 @@ public UnixFileSystem( /// public Task> GetEntriesAsync(IUnixDirectoryEntry directoryEntry, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var dirEntry = (UnixDirectoryEntry)directoryEntry; + var dirInfo = dirEntry.Info; + var entries = dirInfo.GetFileSystemEntries().Select(x => CreateEntry(dirEntry, x)).ToList(); + return Task.FromResult>(entries); } /// public Task GetEntryByNameAsync(IUnixDirectoryEntry directoryEntry, string name, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var dirEntry = (UnixDirectoryEntry)directoryEntry; + var dirInfo = dirEntry.Info; + var entry = dirInfo.GetFileSystemEntries($"^{Regex.Escape(name)}$") + .Select(x => CreateEntry(dirEntry, x)) + .SingleOrDefault(); + return Task.FromResult(entry); } /// @@ -57,13 +74,26 @@ public Task MoveAsync( string fileName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var sourceInfo = ((UnixFileSystemEntry)source).GenericInfo; + var targetEntry = (UnixDirectoryEntry)target; + var targetInfo = targetEntry.Info; + var sourceEntryName = sourceInfo.FullName; + var targetEntryName = UnixPath.Combine(targetInfo.FullName, fileName); + if (Stdlib.rename(sourceEntryName, targetEntryName) == -1) + { + throw new InvalidOperationException("The entry couldn't be moved."); + } + + var targetEntryInfo = UnixFileSystemInfo.GetFileSystemEntry(targetEntryName); + return Task.FromResult(CreateEntry(targetEntry, targetEntryInfo)); } /// public Task UnlinkAsync(IUnixFileSystemEntry entry, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var entryInfo = ((UnixFileSystemEntry)entry).GenericInfo; + entryInfo.Delete(); + return Task.CompletedTask; } /// @@ -72,35 +102,66 @@ public Task CreateDirectoryAsync( string directoryName, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var targetEntry = (UnixDirectoryEntry)targetDirectory; + var newDirectoryName = UnixPath.Combine(targetEntry.Info.FullName, directoryName); + var newDirectoryInfo = new UnixDirectoryInfo(newDirectoryName); + newDirectoryInfo.Create(); + return Task.FromResult((IUnixDirectoryEntry)CreateEntry(targetEntry, newDirectoryInfo)); } /// public Task OpenReadAsync(IUnixFileEntry fileEntry, long startPosition, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var fileInfo = ((UnixFileEntry)fileEntry).Info; + var stream = fileInfo.OpenRead(); + if (startPosition != 0) + { + stream.Seek(startPosition, SeekOrigin.Begin); + } + + return Task.FromResult(stream); } /// - public Task AppendAsync(IUnixFileEntry fileEntry, long? startPosition, Stream data, CancellationToken cancellationToken) + public async Task AppendAsync(IUnixFileEntry fileEntry, long? startPosition, Stream data, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var fileInfo = ((UnixFileEntry)fileEntry).Info; + var stream = fileInfo.Open(FileMode.Append); + if (startPosition != null) + { + stream.Seek(startPosition.Value, SeekOrigin.Begin); + } + + await data.CopyToAsync(stream, 81920, cancellationToken); + + return null; } /// - public Task CreateAsync( + public async Task CreateAsync( IUnixDirectoryEntry targetDirectory, string fileName, Stream data, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var targetInfo = ((UnixDirectoryEntry)targetDirectory).Info; + var fileInfo = new UnixFileInfo(UnixPath.Combine(targetInfo.FullName, fileName)); + var stream = fileInfo.Open(FileMode.CreateNew, FileAccess.Write, FilePermissions.DEFFILEMODE); + + await data.CopyToAsync(stream, 81920, cancellationToken); + + return null; } /// - public Task ReplaceAsync(IUnixFileEntry fileEntry, Stream data, CancellationToken cancellationToken) + public async Task ReplaceAsync(IUnixFileEntry fileEntry, Stream data, CancellationToken cancellationToken) { - throw new NotImplementedException(); + var fileInfo = ((UnixFileEntry)fileEntry).Info; + var stream = fileInfo.Open(FileMode.Create, FileAccess.Write, FilePermissions.DEFFILEMODE); + + await data.CopyToAsync(stream, 81920, cancellationToken); + + return null; } /// @@ -111,7 +172,62 @@ public Task SetMacTimeAsync( DateTimeOffset? create, CancellationToken cancellationToken) { - throw new NotImplementedException(); + if (access == null && modify == null) + { + return Task.FromResult(entry); + } + + var entryInfo = ((UnixFileSystemEntry)entry).GenericInfo; + + var times = new Timeval[2]; + + if (access != null) + { + times[0] = ToTimeval(access.Value.UtcDateTime); + } + else + { + times[0] = ToTimeval(entryInfo.LastAccessTimeUtc); + } + + if (modify != null) + { + times[1] = ToTimeval(modify.Value.UtcDateTime); + } + else + { + times[1] = ToTimeval(entryInfo.LastWriteTimeUtc); + } + + Syscall.utimes(entryInfo.FullName, times); + + entryInfo.Refresh(); + return Task.FromResult(entry); + } + + private static Timeval ToTimeval(DateTime timestamp) + { + var accessTicks = timestamp.ToUniversalTime().Ticks - NativeConvert.UnixEpoch.Ticks; + var seconds = accessTicks / 10_000_000; + var microseconds = (accessTicks % 10_000_000) / 10; + return new Timeval() + { + tv_sec = seconds, + tv_usec = microseconds, + }; + } + + private IUnixFileSystemEntry CreateEntry(UnixDirectoryEntry parent, UnixFileSystemInfo info) + { + switch (info) + { + case UnixFileInfo fileInfo: + return new UnixFileEntry(fileInfo); + case UnixDirectoryInfo dirInfo: + return new UnixDirectoryEntry(dirInfo, _user, _userInfo, parent); + default: + throw new NotSupportedException($"Unsupported file system info type {info}"); + } } } } diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemEntry.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemEntry.cs index 607a4be3..5aeb5102 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemEntry.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemEntry.cs @@ -4,8 +4,6 @@ using System; -using FubarDev.FtpServer.AccountManagement; - using JetBrains.Annotations; using Mono.Unix; @@ -19,10 +17,15 @@ internal abstract class UnixFileSystemEntry : IUnixFileSystemEntry protected UnixFileSystemEntry( [NotNull] UnixFileSystemInfo info) { - _info = info; + GenericInfo = _info = info; Permissions = new UnixPermissions(info); } + /// + /// Gets generic unix file system entry information. + /// + public UnixFileSystemInfo GenericInfo { get; } + /// public string Owner => _info.OwnerUser.UserName; @@ -36,7 +39,7 @@ protected UnixFileSystemEntry( public IUnixPermissions Permissions { get; } /// - public DateTimeOffset? LastWriteTime => _info.LastAccessTimeUtc; + public DateTimeOffset? LastWriteTime => _info.LastWriteTimeUtc; /// public DateTimeOffset? CreatedTime => _info.LastStatusChangeTimeUtc; diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemProvider.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemProvider.cs index cb5e04f6..fcc0183d 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemProvider.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFileSystemProvider.cs @@ -3,15 +3,15 @@ // using System; -using System.Diagnostics; using System.Threading.Tasks; -using JetBrains.Annotations; - using Mono.Unix; namespace FubarDev.FtpServer.FileSystem.Unix { + /// + /// A file system provider that uses the Posix API. + /// public class UnixFileSystemProvider : IFileSystemClassFactory { /// @@ -25,7 +25,7 @@ public Task Create(IAccountInformation accountInformation) var root = new UnixDirectoryInfo(userInfo.HomeDirectory); var rootEntry = new UnixDirectoryEntry(root, accountInformation.User, userInfo); - return Task.FromResult(new UnixFileSystem(rootEntry, accountInformation.User)); + return Task.FromResult(new UnixFileSystem(rootEntry, accountInformation.User, userInfo)); } } } diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixFsFtpServerBuilderExtensions.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFsFtpServerBuilderExtensions.cs new file mode 100644 index 00000000..5400abd6 --- /dev/null +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixFsFtpServerBuilderExtensions.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Fubar Development Junker. All rights reserved. +// + +using FubarDev.FtpServer.FileSystem; +using FubarDev.FtpServer.FileSystem.Unix; + +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace FubarDev.FtpServer +{ + /// + /// Extension methods for . + /// + public static class UnixFsFtpServerBuilderExtensions + { + /// + /// Uses the Unix file system API. + /// + /// The server builder used to configure the FTP server. + /// the server builder used to configure the FTP server. + public static IFtpServerBuilder UseUnixFileSystem(this IFtpServerBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + } +} diff --git a/src/FubarDev.FtpServer.FileSystem.Unix/UnixPermissionExtensions.cs b/src/FubarDev.FtpServer.FileSystem.Unix/UnixPermissionExtensions.cs index e6b2b9eb..56885391 100644 --- a/src/FubarDev.FtpServer.FileSystem.Unix/UnixPermissionExtensions.cs +++ b/src/FubarDev.FtpServer.FileSystem.Unix/UnixPermissionExtensions.cs @@ -16,9 +16,10 @@ internal static class UnixPermissionExtensions public static IAccessMode GetEffectivePermissions( this IUnixDirectoryEntry entry, IFtpUser ftpUser, - UnixUserInfo userInfo) + long userId, + long groupId) { - if (userInfo.UserId == 0 || userInfo.GroupId == 0) + if (userId == 0 || groupId == 0) { return new GenericAccessMode(true, true, true); }