From 901f69d3f36f61d0b1124479faf78db73a78204f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 21 Oct 2024 15:35:36 +0800 Subject: [PATCH 1/2] Support XRMD,XMKD,XPWD --- .gitignore | 2 ++ Library/Connections/ControlConnection.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 3c4efe2..07f88e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. +RunConfig.xml + # User-specific files *.suo *.user diff --git a/Library/Connections/ControlConnection.cs b/Library/Connections/ControlConnection.cs index 736441f..67d2571 100644 --- a/Library/Connections/ControlConnection.cs +++ b/Library/Connections/ControlConnection.cs @@ -235,6 +235,7 @@ private async Task ProcessCommandAsync(string message) await fileProvider.DeleteAsync(parameter); await ReplyAsync(FtpReplyCode.FileActionOk, "Delete succeeded"); return; + case "XRMD": case "RMD": if (!authenticated) { @@ -244,6 +245,7 @@ private async Task ProcessCommandAsync(string message) await fileProvider.DeleteDirectoryAsync(parameter); await ReplyAsync(FtpReplyCode.FileActionOk, "Directory deleted"); return; + case "XMKD": case "MKD": if (!authenticated) { @@ -257,6 +259,7 @@ await ReplyAsync( "\"{0}\"", fileProvider.GetWorkingDirectory().Replace("\"", "\"\""))); return; + case "XPWD": case "PWD": if (!authenticated) { From a0c36f600ebcbc70f8f39cd6e486c1b3be8215c0 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Oct 2024 11:21:11 +0800 Subject: [PATCH 2/2] Add PASV mode options and hybrid auth type. --- Library/Authenticate/FtpUser.cs | 26 +++++++++++ Library/Authenticate/HybridAuthenticator.cs | 43 +++++++++++++++++++ Library/Connections/ControlConnection.cs | 15 +++++-- Library/Connections/IDataConnectionFactory.cs | 4 +- Library/Connections/LocalDataConnection.cs | 21 +++++++-- .../Connections/LocalDataConnectionFactory.cs | 6 ++- Library/Connections/SslLocalDataConnection.cs | 12 ++++-- .../SslLocalDataConnectionFactory.cs | 6 ++- Library/FtpServer.cs | 13 ++++-- Library/Options/FtpServerOptions.cs | 31 +++++++++++++ Library/README.md | 5 ++- Server/Program.cs | 12 +++++- Server/RunConfig.cs | 25 +++++++++++ 13 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 Library/Authenticate/FtpUser.cs create mode 100644 Library/Authenticate/HybridAuthenticator.cs create mode 100644 Library/Options/FtpServerOptions.cs diff --git a/Library/Authenticate/FtpUser.cs b/Library/Authenticate/FtpUser.cs new file mode 100644 index 0000000..0f7a798 --- /dev/null +++ b/Library/Authenticate/FtpUser.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Zhaoquan Huang. All rights reserved +// + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Zhaobang.FtpServer.Authenticate +{ + /// + /// Ftp authenticator user model. + /// + public class FtpUser + { + /// + /// Gets or sets user name. + /// + public string Name { get; set; } + + /// + /// Gets or sets user password. + /// + public string Password { get; set; } + } +} diff --git a/Library/Authenticate/HybridAuthenticator.cs b/Library/Authenticate/HybridAuthenticator.cs new file mode 100644 index 0000000..d7f060f --- /dev/null +++ b/Library/Authenticate/HybridAuthenticator.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Zhaoquan Huang. All rights reserved +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Zhaobang.FtpServer.Authenticate +{ + /// + /// The authenticator that accepts both userName/password and anonymous. + /// + public class HybridAuthenticator : IAuthenticator + { + private readonly List users; + private readonly bool enableAnonymous; + + /// + /// Initializes a new instance of the class. + /// + /// The users to accept. + /// Enable anonymous mode or not. + public HybridAuthenticator(List users, bool enableAnonymous) + { + this.users = users; + this.enableAnonymous = enableAnonymous; + } + + /// + /// Verifies if the username-password pair is correct. + /// + /// The user name user inputted. + /// The password user inputted. + /// Whether the pair is correct. + public bool Authenticate(string userName, string password) + { + if (this.enableAnonymous && userName.ToUpper() == "ANONYMOUS") return true; + return users.Any(u => u.Name.ToUpper() == userName.ToUpper() && u.Password.ToUpper() == password.ToUpper()); + } + } +} diff --git a/Library/Connections/ControlConnection.cs b/Library/Connections/ControlConnection.cs index 67d2571..e3bdd7c 100644 --- a/Library/Connections/ControlConnection.cs +++ b/Library/Connections/ControlConnection.cs @@ -14,6 +14,7 @@ using Zhaobang.FtpServer.Connections; using Zhaobang.FtpServer.Exceptions; using Zhaobang.FtpServer.File; +using Zhaobang.FtpServer.Options; namespace Zhaobang.FtpServer.Connections { @@ -30,6 +31,7 @@ internal class ControlConnection : IDisposable private readonly IPEndPoint remoteEndPoint; private readonly IPEndPoint localEndPoint; + private readonly FtpServerOptions ftpServerOptions; /// /// This should be available all time, but needs to check @@ -78,7 +80,8 @@ internal class ControlConnection : IDisposable /// /// The that creates the connection. /// The TCP client of the connection. - internal ControlConnection(FtpServer server, TcpClient tcpClient) + /// The ftp options. + internal ControlConnection(FtpServer server, TcpClient tcpClient, FtpServerOptions ftpServerOptions) { this.server = server; this.tcpClient = tcpClient; @@ -90,8 +93,8 @@ internal ControlConnection(FtpServer server, TcpClient tcpClient) var localUri = new Uri("ftp://" + this.tcpClient.Client.LocalEndPoint.ToString()); localEndPoint = new IPEndPoint(IPAddress.Parse(localUri.Host), localUri.Port); - - dataConnection = server.DataConnector.GetDataConnection(localEndPoint.Address); + this.ftpServerOptions = ftpServerOptions; + dataConnection = server.DataConnector.GetDataConnection(localEndPoint.Address, ftpServerOptions.PassiveMinPort, ftpServerOptions.PassiveMaxPort); stream = this.tcpClient.GetStream(); } @@ -657,7 +660,11 @@ await ReplyAsync( private async Task CommandPasvAsync() { var localEP = dataConnection.Listen(); - var ipBytes = localEP.Address.GetAddressBytes(); + + // var ipBytes = localEP.Address.GetAddressBytes(); + var passiveIpValid = IPAddress.TryParse(ftpServerOptions.PassiveIp, out var pasvIP); + if (!passiveIpValid) pasvIP = localEP.Address; + var ipBytes = pasvIP.GetAddressBytes(); if (ipBytes.Length != 4) throw new Exception(); var passiveEPString = string.Format( diff --git a/Library/Connections/IDataConnectionFactory.cs b/Library/Connections/IDataConnectionFactory.cs index 883231e..4e676bc 100644 --- a/Library/Connections/IDataConnectionFactory.cs +++ b/Library/Connections/IDataConnectionFactory.cs @@ -18,7 +18,9 @@ public interface IDataConnectionFactory /// Gets for a control connection. /// /// The server IP that was connected by the user. + /// The min port in PASSIVE mode. + /// The max port in PASSIVE mode. /// The for that control connection. - IDataConnection GetDataConnection(IPAddress localIP); + IDataConnection GetDataConnection(IPAddress localIP, int minPort, int maxPort); } } diff --git a/Library/Connections/LocalDataConnection.cs b/Library/Connections/LocalDataConnection.cs index f6b19d1..0091316 100644 --- a/Library/Connections/LocalDataConnection.cs +++ b/Library/Connections/LocalDataConnection.cs @@ -18,9 +18,12 @@ namespace Zhaobang.FtpServer.Connections /// public class LocalDataConnection : IDisposable, IDataConnection { - private const int MinPort = 1024; - private const int MaxPort = 65535; - private static int lastUsedPort = new Random().Next(MinPort, MaxPort); +#pragma warning disable SA1306 // Field names should begin with lower-case letter + private static int MinPort = 1024; + private static int MaxPort = 65535; +#pragma warning restore SA1306 // Field names should begin with lower-case letter + private static object portLock = new object(); + private static int lastUsedPort = 0; // new Random().Next(MinPort, MaxPort); private readonly IPAddress listeningIP; @@ -39,8 +42,18 @@ public class LocalDataConnection : IDisposable, IDataConnection /// NO connection will be initiated immediately. /// /// The IP which was connected by the user. - public LocalDataConnection(IPAddress localIP) + /// The min port in PASSIVE mode. + /// The max port in PASSIVE mode. + public LocalDataConnection(IPAddress localIP, int minPort, int maxPort) { + MinPort = minPort; + MaxPort = maxPort; + if (lastUsedPort == 0) + { + lock (portLock) + if (lastUsedPort == 0) + lastUsedPort = new Random().Next(MinPort, MaxPort); + } listeningIP = localIP; } diff --git a/Library/Connections/LocalDataConnectionFactory.cs b/Library/Connections/LocalDataConnectionFactory.cs index e228b71..ecab475 100644 --- a/Library/Connections/LocalDataConnectionFactory.cs +++ b/Library/Connections/LocalDataConnectionFactory.cs @@ -21,10 +21,12 @@ public class LocalDataConnectionFactory : IDataConnectionFactory /// Gets for a user. /// /// The IP which was connected by the user. + /// The min port in PASSIVE mode. + /// The max port in PASSIVE mode. /// The data connection for the user. - public IDataConnection GetDataConnection(IPAddress localIP) + public IDataConnection GetDataConnection(IPAddress localIP, int minPort, int maxPort) { - return new LocalDataConnection(localIP); + return new LocalDataConnection(localIP, minPort, maxPort); } } } diff --git a/Library/Connections/SslLocalDataConnection.cs b/Library/Connections/SslLocalDataConnection.cs index 955e0ae..e0e583b 100644 --- a/Library/Connections/SslLocalDataConnection.cs +++ b/Library/Connections/SslLocalDataConnection.cs @@ -20,8 +20,10 @@ namespace Zhaobang.FtpServer.Connections /// public class SslLocalDataConnection : IDisposable, IDataConnection, ISslDataConnection { - private const int MinPort = 1024; - private const int MaxPort = 65535; +#pragma warning disable SA1306 // Field names should begin with lower-case letter + private static int MinPort = 1024; + private static int MaxPort = 65535; +#pragma warning restore SA1306 // Field names should begin with lower-case letter private static int lastUsedPort = new Random().Next(MinPort, MaxPort); private readonly IPAddress listeningIP; @@ -43,8 +45,12 @@ public class SslLocalDataConnection : IDisposable, IDataConnection, ISslDataConn /// /// The IP which was connected by the user. /// The certificate to upgrade to encrypted stream. - public SslLocalDataConnection(IPAddress localIP, X509Certificate certificate) + /// The min port for PASV mode. + /// The max port for PASV mode. + public SslLocalDataConnection(IPAddress localIP, X509Certificate certificate, int minPort, int maxPort) { + MinPort = minPort; + MaxPort = maxPort; listeningIP = localIP; this.certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); } diff --git a/Library/Connections/SslLocalDataConnectionFactory.cs b/Library/Connections/SslLocalDataConnectionFactory.cs index 842d532..fd11047 100644 --- a/Library/Connections/SslLocalDataConnectionFactory.cs +++ b/Library/Connections/SslLocalDataConnectionFactory.cs @@ -31,10 +31,12 @@ public SslLocalDataConnectionFactory(X509Certificate certificate) /// Gets the data connection instance. /// /// The local IP to bind the socket. + /// The min port for PASV mode. + /// The max port for PASV mode. /// The created data connection instance. - public IDataConnection GetDataConnection(IPAddress localIP) + public IDataConnection GetDataConnection(IPAddress localIP, int minPort, int maxPort) { - return new SslLocalDataConnection(localIP, certificate); + return new SslLocalDataConnection(localIP, certificate, minPort, maxPort); } } } diff --git a/Library/FtpServer.cs b/Library/FtpServer.cs index 2e3ec30..fa3ee91 100644 --- a/Library/FtpServer.cs +++ b/Library/FtpServer.cs @@ -14,6 +14,7 @@ using Zhaobang.FtpServer.Authenticate; using Zhaobang.FtpServer.Connections; using Zhaobang.FtpServer.File; +using Zhaobang.FtpServer.Options; using Zhaobang.FtpServer.Trace; namespace Zhaobang.FtpServer @@ -29,6 +30,8 @@ public sealed class FtpServer private readonly IControlConnectionSslFactory controlConnectionSslFactory; private readonly FtpTracer tracer = new FtpTracer(); + private readonly FtpServerOptions ftpServerOptions; + private IPEndPoint endPoint; private TcpListener tcpListener; @@ -57,7 +60,7 @@ public FtpServer( IFileProviderFactory fileProviderFactory, IDataConnectionFactory dataConnFactory, IAuthenticator authenticator) - : this(endPoint, fileProviderFactory, dataConnFactory, authenticator, null) + : this(endPoint, fileProviderFactory, dataConnFactory, authenticator, null, new FtpServerOptions()) { } @@ -70,12 +73,14 @@ public FtpServer( /// The to use. /// The to use. /// The to upgrade control connection to SSL. + /// The set ftp options. public FtpServer( IPEndPoint endPoint, IFileProviderFactory fileProviderFactory, IDataConnectionFactory dataConnFactory, IAuthenticator authenticator, - IControlConnectionSslFactory controlConnectionSslFactory) + IControlConnectionSslFactory controlConnectionSslFactory, + FtpServerOptions ftpServerOptions) { this.endPoint = endPoint; tcpListener = new TcpListener(endPoint); @@ -85,6 +90,8 @@ public FtpServer( this.authenticator = authenticator; this.controlConnectionSslFactory = controlConnectionSslFactory; + this.ftpServerOptions = ftpServerOptions; + tracer.CommandInvoked += Tracer_CommandInvoked; tracer.ReplyInvoked += Tracer_ReplyInvoked; } @@ -139,7 +146,7 @@ public async Task RunAsync(CancellationToken cancellationToken) try { - ControlConnection handler = new ControlConnection(this, tcpClient); + ControlConnection handler = new ControlConnection(this, tcpClient, ftpServerOptions); var result = handler.RunAsync(cancellationToken); } catch (Exception) diff --git a/Library/Options/FtpServerOptions.cs b/Library/Options/FtpServerOptions.cs new file mode 100644 index 0000000..21f649b --- /dev/null +++ b/Library/Options/FtpServerOptions.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Zhaoquan Huang. All rights reserved +// + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Zhaobang.FtpServer.Options +{ + /// + /// Ftp server options. + /// + public class FtpServerOptions + { + /// + /// Gets or sets the min port of the FTP file system in passive mode. + /// + public string PassiveIp { get; set; } = string.Empty; + + /// + /// Gets or sets the min port of the FTP file system in passive mode. + /// + public int PassiveMinPort { get; set; } = 1024; + + /// + /// Gets or sets the max port of the FTP file system in passive mode. + /// + public int PassiveMaxPort { get; set; } = 65535; + } +} diff --git a/Library/README.md b/Library/README.md index f571ca8..f2f0286 100644 --- a/Library/README.md +++ b/Library/README.md @@ -49,7 +49,8 @@ var server = new FtpServer( new MyFileProviderFactory(), new MyDataConnectionFactory(), new MyAuthenticator(), - new MyControlConnectionSslFactory() + new MyControlConnectionSslFactory(), + new FtpServerOptions() ); // the remaining is same as simple use ``` @@ -65,7 +66,7 @@ var fileProviderFactory = new SimpleFileProviderFactory(config.BaseDirectory); var dataConnectionFactory = new SslLocalDataConnectionFactory(certificate); var authenticator = new AnonymousAuthenticator(); var controlConnectionSslFactory = new ControlConnectionSslFactory(certificate); -var server = new FtpServer(ep, fileProviderFactory, dataConnectionFactory, authenticator, controlConnectionSslFactory); +var server = new FtpServer(ep, fileProviderFactory, dataConnectionFactory, authenticator, controlConnectionSslFactory, new FtpServerOptions()); ``` The .NET Standard 1.4 version requires your own implementation of those classes. You can refer to the source code for a sample implementation. \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index c841d90..624ff3a 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -14,6 +14,7 @@ using Zhaobang.FtpServer.Authenticate; using Zhaobang.FtpServer.Connections; using Zhaobang.FtpServer.File; +using Zhaobang.FtpServer.Options; namespace Zhaobang.FtpServer { @@ -56,6 +57,13 @@ private static void Main(string[] args) } } + var ftpServerOptions = new FtpServerOptions + { + PassiveIp = config.PassiveIp, + PassiveMinPort = config.PassiveMinPort, + PassiveMaxPort = config.PassiveMaxPort, + }; + X509Certificate certificate = null; try { @@ -72,10 +80,10 @@ private static void Main(string[] args) var fileProviderFactory = new SimpleFileProviderFactory(config.BaseDirectory); var dataConnectionFactory = certificate == null ? new LocalDataConnectionFactory() : (IDataConnectionFactory)new SslLocalDataConnectionFactory(certificate); - var authenticator = new AnonymousAuthenticator(); + var authenticator = new HybridAuthenticator(config.FtpUsers, config.EnableAnonymous); var controlConnectionSslFactory = certificate == null ? null : new ControlConnectionSslFactory(certificate); - var server = new FtpServer(ep, fileProviderFactory, dataConnectionFactory, authenticator, controlConnectionSslFactory); + var server = new FtpServer(ep, fileProviderFactory, dataConnectionFactory, authenticator, controlConnectionSslFactory, ftpServerOptions); servers.Add(server); return server.RunAsync(cancelSource.Token) diff --git a/Server/RunConfig.cs b/Server/RunConfig.cs index 830f242..aea8455 100644 --- a/Server/RunConfig.cs +++ b/Server/RunConfig.cs @@ -8,6 +8,7 @@ using System.Security; using System.Text; using System.Xml.Serialization; +using Zhaobang.FtpServer.Authenticate; namespace Zhaobang.FtpServer { @@ -34,6 +35,11 @@ public static RunConfig Default Address = IPAddress.IPv6Any, Port = 21, }); + config.FtpUsers.Add(new FtpUser + { + Name = "user", + Password = "password", + }); return config; } } @@ -58,6 +64,25 @@ public static RunConfig Default /// public string CertificatePassword { get; set; } = string.Empty; + /// + /// Gets or sets the min port of the FTP file system in passive mode. + /// + public string PassiveIp { get; set; } = string.Empty; + + /// + /// Gets or sets the min port of the FTP file system in passive mode. + /// + public int PassiveMinPort { get; set; } = 1024; + + /// + /// Gets or sets the max port of the FTP file system in passive mode. + /// + public int PassiveMaxPort { get; set; } = 65535; + + public bool EnableAnonymous { get; set; } = false; + + public List FtpUsers { get; set; } = new List(); + /// /// An end point to listen on. ///