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/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 736441f..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();
}
@@ -235,6 +238,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 +248,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 +262,7 @@ await ReplyAsync(
"\"{0}\"",
fileProvider.GetWorkingDirectory().Replace("\"", "\"\"")));
return;
+ case "XPWD":
case "PWD":
if (!authenticated)
{
@@ -654,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.
///