FubarDevelopment / FtpServer

Portable FTP server written in .NET
http://fubardevelopment.github.io/FtpServer/
MIT License
482 stars 163 forks source link

Callback when command processed #14

Closed danpowell88 closed 6 years ago

danpowell88 commented 7 years ago

Is there anyway to run some custom code after certain commands have run.

IE. I'd like to kick off another part of my app after a file has been uploaded

Only way so far I can tell would be to create my own commandhandlerfactory and provide my own implementations of the existing commands using inheritance.

fubar-coder commented 7 years ago

You're right that this isn't supported yet. Implementing your own IFtpCommandHandlerFactory and returning your own command (which redirects every property and function to the original handler) is currently the only way to achieve this.

I'm currently developing a rewrite of the FTP server using DotNetty which should also support .NET Standard 1.3 and this could be easily done, because you can modify the DotNetty pipeline and intercept the FTP commands (and their responses).

lsvhome commented 7 years ago
//-------------------
    public class CustomAssemblyFtpCommandHandlerFactory : FubarDev.FtpServer.AssemblyFtpCommandHandlerFactory
    {
        public CustomAssemblyFtpCommandHandlerFactory([NotNull] Assembly assembly, [ItemNotNull, NotNull] params Assembly[] assemblies) : base(assembly, assemblies)
        {
        }

        public override IEnumerable<FtpCommandHandlerExtension> CreateCommandHandlerExtensions(FtpConnection connection)
        {
            return base.CreateCommandHandlerExtensions(connection);
        }

        public override IEnumerable<FtpCommandHandler> CreateCommandHandlers(FtpConnection connection)
        {
            var baseHandlers = base.CreateCommandHandlers(connection);
            var ret = baseHandlers.Select(each => new CustomCommandDecorator(each, connection, each.Names.First(), each.Names.Skip(1).ToArray()));
            return ret;
        }

        public class CustomCommandDecorator : FtpCommandHandler
        {
            FtpCommandHandler baseFtpCommandHandler;

            public CustomCommandDecorator([NotNull] FtpCommandHandler baseFtpCommandHandler, [NotNull] FtpConnection connection, [NotNull] string name, [ItemNotNull, NotNull] params string[] alternativeNames) : base(connection, name, alternativeNames)
            {
                this.baseFtpCommandHandler = baseFtpCommandHandler;
            }

            public async override Task<FtpResponse> Process([NotNull] FtpCommand command, CancellationToken cancellationToken)
            {
                try
                {
                    System.Diagnostics.Debug.WriteLine("FTP COMMAND: " + command.ToString());
                    var ret = await this.baseFtpCommandHandler.Process(command, cancellationToken);
                    System.Diagnostics.Debug.WriteLine("FTP COMMAND RESULT: " + ret);
                    return ret;
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.Fail(ex.ToString());
                    throw;
                }
            }

            public override bool IsAbortable
            {
                get
                {
                    return this.baseFtpCommandHandler.IsAbortable;
                }
            }

            public override bool IsLoginRequired
            {
                get
                {
                    return this.baseFtpCommandHandler.IsLoginRequired;
                }
            }
        }
    }
//-----------------------

        protected virtual void StartFtpServer(FubarDev.FtpServer.FileSystem.IFileSystemClassFactory fileSystemProvider)
        {
            //// Load server certificate
            //// var cert = new X509Certificate2("test.pfx");
            //// AuthTlsCommandHandler.ServerCertificate = cert;

            //// Only allow anonymous login
            var membershipProvider = new AnonymousMembershipProvider(new NoValidation());

            //// Use all commands from the FtpServer assembly and the one(s) from the AuthTls assembly
            var commandFactory = new CustomAssemblyFtpCommandHandlerFactory(typeof(FtpServer).Assembly, typeof(AuthTlsCommandHandler).Assembly);

            //// Initialize the FTP server
            this.ftpServer = new FtpServer(fileSystemProvider, membershipProvider, "127.0.0.1", (int)this.port, commandFactory)
            {
                DefaultEncoding = Encoding.Default,
                LogManager = new FtpLogManager(),
            };

#if USE_FTPS_IMPLICIT
                //// Use an implicit SSL connection (without the AUTHTLS command)
                ftpServer.ConfigureConnection += (s, e) =>
                {
                    var sslStream = new FixedSslStream(e.Connection.OriginalStream);
                    sslStream.AuthenticateAsServer(cert);
                    e.Connection.SocketStream = sslStream;
                };
#endif

            try
            {
                this.ftpServer.Start();
            }
            catch (Exception ex)
            {
                ex.Process();
            }
        }
//------------------------
aavanesov commented 5 years ago

Hello. Do you have any update regarding this issue? It looks like the code sample provided is not compatible with version 3 any more...

fubar-coder commented 5 years ago

You can intercept a command by implementing either a IFtpMiddleware or an IFtpCommandMiddleware. The difference between the two is described in the upgrade guide.

That way, you can intercept the FTP command (and replace it with your own implementation), but you cannot intercept the response, because those is returned through a different channel. To intercept the FTP servers responses, you would need to implement your own IServerCommandFeature and set this feature during the connection initialization in the IFtpServer.ConfigureConnection event. Your implementation would need to pass all commands from the IServerCommand channel to the original channel writer. The server command you'd most likely want to intercept is SendResponseServerCommand.

The main reason for decoupling command execution and sending the response was that I wanted to streamline communication between the connection and the core server functionality. This feature was extremely important for the AUTH TLS and REIN implementations.

EDIT: Clarifications around IServerCommandFeature.

aavanesov commented 5 years ago

For my case I need to catch the STOR command and get the file path on the server and its size. I was able to get the file name with the IFtpMiddleware/IFtpCommandMiddleware but not the size of an uploaded file.

What is the best way to do it?

fubar-coder commented 5 years ago

The file size isn't part of the FTP protocol and the only way to handle this use case is to implement your own STOR command handler. IFtpCommandHandlerProvider is the new extension point where you return the new FTP commands.

EDIT: You may get the file size after the FTP command is run (and I suggest that you use IFtpCommandMiddleware) by accessing the destination path/name of the uploaded file. This should be the easiest way.

Example (complicated way)

Custom FTP command handler provider

class MyFtpCommnandHandlerProvider : IFtpCommandHandlerProvider
{
    public MyFtpCommnandHandlerProvider(DefaultFtpCommandHandlerProvider defaultProvider)
    {
        IFtpCommandHandlerInformation yourStorCommandInformation = ... /* needs to be implemented by you */;
        CommandHandlers = defaultProvider.CommandHandlers
                .Where(x => x.Name != "STOR")
                .Concat(new[] { yourStorCommandInformation }
                .ToList();
    }

    /// <inheritdoc />
    public IEnumerable<IFtpCommandHandlerInformation> CommandHandlers { get; }
}

Changes to the registration

services
    .AddFtpServer(... /* yadda, yadda */)
    /* Add your custom FTP command handler provider */
    .AddScoped<IFtpCommandHandlerProvider, MyFtpCommandHandlerProvider>()
    /* Register the default provider to get the default FTP command handlers */
    .AddScoped<DefaultFtpCommandHandlerProvider>();
aavanesov commented 5 years ago
  1. It looks like InvokeAsync of IFtpCommandMiddleware is called before the file is uploaded. Therefore, I cannot obtain the file size using the path of the uploaded file.
  2. Is there any sample which can be used as a starting point for custom implementation of IFtpCommandHandlerInformation?
fubar-coder commented 5 years ago
  1. You can. Yes, it's called before it's uploaded, but you can await the end of the command execution. Yes. you're right. It doesn't wait until the command is finished, because it's an abortable (background) command.

  2. There are two interfaces that you can implement: IFtpCommandHandlerInformation and IFtpCommandHandlerInstanceInformation, but you have to register it always as IFtpCommandHandlerInformation.

A simple implementation might look like:

class MyStorCommandHandlerInformation : IFtpCommandHandlerInformation
{
    /// <inheritdoc />
    public string Name { get; } = "STOR";

    /// <inheritdoc />
    public bool IsLoginRequired { get; } = true;

    /// <inheritdoc />
    public bool IsAbortable { get; } = true;

    /// <inheritdoc />
    public Type Type { get; } = typeof(MyStorCommandHandler);

    /// <inheritdoc />
    public bool IsExtensible { get; } = false;
}

Now you have to implement MyStorCommandHandler. You may copy the original implementation and modify it.

fubar-coder commented 5 years ago

Ok, I took a look at the source code and I was right. You can do the following in your IFtpCommandMiddleware:

class YourMiddleware : IFtpCommandMiddleware
{
    public async Task InvokeAsync(FtpExecutionContext context, FtpCommandExecutionDelegate next)
    {
        // Do some stuff before command gets executed
        await next(context);
        // Do some stuff after the command got executed
    }
}

The above works even when the command is a background command. The code after the await will definitely be executed after the command was processed.

aavanesov commented 5 years ago

Thanks a lot for your clarifications. I was able to implement my logic with a custom IFtpCommandMiddleware.

justinstenning commented 4 years ago

The above works even when the command is a background command. The code after the await will definitely be executed after the command was processed.

Yes the commands are all hit before the next line of code, however testing this with the S3 provider and no, the code after the await next(context) continues BEFORE the S3 provider uploads the file (e.g. before CreateAsync is called). Is there something else I'm missing perhaps? Or is the only reliable way to do this with a custom provider?

I really think it would be a common requirement to perform some action after the successfull completion of an upload/delete whatever. Perhaps an additional middleware that occurs afterwards or an addition to the IFtpCommandMiddleware?