Closed danpowell88 closed 6 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).
//-------------------
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();
}
}
//------------------------
Hello. Do you have any update regarding this issue? It looks like the code sample provided is not compatible with version 3 any more...
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
.
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?
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.
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; }
}
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>();
You can. Yes, it's called before it's uploaded, but you can
Yes. you're right. It doesn't wait until the command is finished, because it's an abortable (background) command.await
the end of the command execution.
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.
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.
Thanks a lot for your clarifications. I was able to implement my logic with a custom IFtpCommandMiddleware.
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?
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.