Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PASV mode options and Hybrid Authenticator #13

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
26 changes: 26 additions & 0 deletions Library/Authenticate/FtpUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// <copyright file="FtpUser.cs" company="Zhaoquan Huang">
// Copyright (c) Zhaoquan Huang. All rights reserved
// </copyright>

using System;
using System.Collections.Generic;
using System.Text;

namespace Zhaobang.FtpServer.Authenticate
{
/// <summary>
/// Ftp authenticator user model.
/// </summary>
public class FtpUser
{
/// <summary>
/// Gets or sets user name.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Gets or sets user password.
/// </summary>
public string Password { get; set; }
}
}
43 changes: 43 additions & 0 deletions Library/Authenticate/HybridAuthenticator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// <copyright file="HybridAuthenticator.cs" company="Zhaoquan Huang">
// Copyright (c) Zhaoquan Huang. All rights reserved
// </copyright>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Zhaobang.FtpServer.Authenticate
{
/// <summary>
/// The authenticator that accepts both userName/password and anonymous.
/// </summary>
public class HybridAuthenticator : IAuthenticator
{
private readonly List<FtpUser> users;
private readonly bool enableAnonymous;

/// <summary>
/// Initializes a new instance of the <see cref="HybridAuthenticator"/> class.
/// </summary>
/// <param name="users">The users to accept.</param>
/// <param name="enableAnonymous">Enable anonymous mode or not.</param>
public HybridAuthenticator(List<FtpUser> users, bool enableAnonymous)
{
this.users = users;
this.enableAnonymous = enableAnonymous;
}

/// <summary>
/// Verifies if the username-password pair is correct.
/// </summary>
/// <param name="userName">The user name user inputted.</param>
/// <param name="password">The password user inputted.</param>
/// <returns>Whether the pair is correct.</returns>
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());
}
}
}
18 changes: 14 additions & 4 deletions Library/Connections/ControlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Zhaobang.FtpServer.Connections;
using Zhaobang.FtpServer.Exceptions;
using Zhaobang.FtpServer.File;
using Zhaobang.FtpServer.Options;

namespace Zhaobang.FtpServer.Connections
{
Expand All @@ -30,6 +31,7 @@ internal class ControlConnection : IDisposable

private readonly IPEndPoint remoteEndPoint;
private readonly IPEndPoint localEndPoint;
private readonly FtpServerOptions ftpServerOptions;

/// <summary>
/// This should be available all time, but needs to check
Expand Down Expand Up @@ -78,7 +80,8 @@ internal class ControlConnection : IDisposable
/// </summary>
/// <param name="server">The <see cref="FtpServer"/> that creates the connection.</param>
/// <param name="tcpClient">The TCP client of the connection.</param>
internal ControlConnection(FtpServer server, TcpClient tcpClient)
/// <param name="ftpServerOptions">The ftp options.</param>
internal ControlConnection(FtpServer server, TcpClient tcpClient, FtpServerOptions ftpServerOptions)
{
this.server = server;
this.tcpClient = tcpClient;
Expand All @@ -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();
}
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -257,6 +262,7 @@ await ReplyAsync(
"\"{0}\"",
fileProvider.GetWorkingDirectory().Replace("\"", "\"\"")));
return;
case "XPWD":
case "PWD":
if (!authenticated)
{
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion Library/Connections/IDataConnectionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public interface IDataConnectionFactory
/// Gets <see cref="IDataConnection"/> for a control connection.
/// </summary>
/// <param name="localIP">The server IP that was connected by the user.</param>
/// <param name="minPort">The min port in PASSIVE mode.</param>
/// <param name="maxPort">The max port in PASSIVE mode.</param>
/// <returns>The <see cref="IDataConnection"/> for that control connection.</returns>
IDataConnection GetDataConnection(IPAddress localIP);
IDataConnection GetDataConnection(IPAddress localIP, int minPort, int maxPort);
}
}
21 changes: 17 additions & 4 deletions Library/Connections/LocalDataConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ namespace Zhaobang.FtpServer.Connections
/// </summary>
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;

Expand All @@ -39,8 +42,18 @@ public class LocalDataConnection : IDisposable, IDataConnection
/// NO connection will be initiated immediately.
/// </summary>
/// <param name="localIP">The IP which was connected by the user.</param>
public LocalDataConnection(IPAddress localIP)
/// <param name="minPort">The min port in PASSIVE mode.</param>
/// <param name="maxPort">The max port in PASSIVE mode.</param>
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;
}

Expand Down
6 changes: 4 additions & 2 deletions Library/Connections/LocalDataConnectionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public class LocalDataConnectionFactory : IDataConnectionFactory
/// Gets <see cref="LocalDataConnection"/> for a user.
/// </summary>
/// <param name="localIP">The IP which was connected by the user.</param>
/// <param name="minPort">The min port in PASSIVE mode.</param>
/// <param name="maxPort">The max port in PASSIVE mode.</param>
/// <returns>The data connection for the user.</returns>
public IDataConnection GetDataConnection(IPAddress localIP)
public IDataConnection GetDataConnection(IPAddress localIP, int minPort, int maxPort)
{
return new LocalDataConnection(localIP);
return new LocalDataConnection(localIP, minPort, maxPort);
}
}
}
12 changes: 9 additions & 3 deletions Library/Connections/SslLocalDataConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ namespace Zhaobang.FtpServer.Connections
/// </summary>
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;
Expand All @@ -43,8 +45,12 @@ public class SslLocalDataConnection : IDisposable, IDataConnection, ISslDataConn
/// </summary>
/// <param name="localIP">The IP which was connected by the user.</param>
/// <param name="certificate">The certificate to upgrade to encrypted stream.</param>
public SslLocalDataConnection(IPAddress localIP, X509Certificate certificate)
/// <param name="minPort">The min port for PASV mode.</param>
/// <param name="maxPort">The max port for PASV mode.</param>
public SslLocalDataConnection(IPAddress localIP, X509Certificate certificate, int minPort, int maxPort)
{
MinPort = minPort;
MaxPort = maxPort;
listeningIP = localIP;
this.certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
}
Expand Down
6 changes: 4 additions & 2 deletions Library/Connections/SslLocalDataConnectionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ public SslLocalDataConnectionFactory(X509Certificate certificate)
/// Gets the data connection instance.
/// </summary>
/// <param name="localIP">The local IP to bind the socket.</param>
/// <param name="minPort">The min port for PASV mode.</param>
/// <param name="maxPort">The max port for PASV mode.</param>
/// <returns>The created data connection instance.</returns>
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);
}
}
}
Expand Down
13 changes: 10 additions & 3 deletions Library/FtpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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())
{
}

Expand All @@ -70,12 +73,14 @@ public FtpServer(
/// <param name="dataConnFactory">The <see cref="IDataConnectionFactory"/> to use.</param>
/// <param name="authenticator">The <see cref="IAuthenticator"/> to use.</param>
/// <param name="controlConnectionSslFactory">The <see cref="IControlConnectionSslFactory"/> to upgrade control connection to SSL.</param>
/// <param name="ftpServerOptions">The <see cref="FtpServerOptions"/> set ftp options.</param>
public FtpServer(
IPEndPoint endPoint,
IFileProviderFactory fileProviderFactory,
IDataConnectionFactory dataConnFactory,
IAuthenticator authenticator,
IControlConnectionSslFactory controlConnectionSslFactory)
IControlConnectionSslFactory controlConnectionSslFactory,
FtpServerOptions ftpServerOptions)
{
this.endPoint = endPoint;
tcpListener = new TcpListener(endPoint);
Expand All @@ -85,6 +90,8 @@ public FtpServer(
this.authenticator = authenticator;
this.controlConnectionSslFactory = controlConnectionSslFactory;

this.ftpServerOptions = ftpServerOptions;

tracer.CommandInvoked += Tracer_CommandInvoked;
tracer.ReplyInvoked += Tracer_ReplyInvoked;
}
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions Library/Options/FtpServerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// <copyright file="FtpServerOptions.cs" company="Zhaoquan Huang">
// Copyright (c) Zhaoquan Huang. All rights reserved
// </copyright>

using System;
using System.Collections.Generic;
using System.Text;

namespace Zhaobang.FtpServer.Options
{
/// <summary>
/// Ftp server options.
/// </summary>
public class FtpServerOptions
{
/// <summary>
/// Gets or sets the min port of the FTP file system in passive mode.
/// </summary>
public string PassiveIp { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the min port of the FTP file system in passive mode.
/// </summary>
public int PassiveMinPort { get; set; } = 1024;

/// <summary>
/// Gets or sets the max port of the FTP file system in passive mode.
/// </summary>
public int PassiveMaxPort { get; set; } = 65535;
}
}
5 changes: 3 additions & 2 deletions Library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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.
Loading