Skip to content

Instantly share code, notes, and snippets.

@fubar-coder
Created April 19, 2016 19:28
Show Gist options
  • Save fubar-coder/32a1a6d859d12a9961e1a7e897a27900 to your computer and use it in GitHub Desktop.
Save fubar-coder/32a1a6d859d12a9961e1a7e897a27900 to your computer and use it in GitHub Desktop.
Modifications to allow PASV to use predefined ports (by Guillaume Pelletier)
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<system.diagnostics>
<trace autoflush="true"/>
<sources>
<source name="System.Net" maxdatasize="1024">
<listeners>
<!--add name="MyTraceFile" /-->
</listeners>
</source>
</sources>
<!--
<sharedListeners>
<add name="MyTraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="System.Net.trace.log"/>
</sharedListeners>
-->
<switches>
<add name="System.Net" value="Verbose"/>
</switches>
</system.diagnostics>
<appSettings>
<add key="port" value="8110"/>
<add key="pasvPorts" value="8108,8109"/>
</appSettings>
</configuration>
//-----------------------------------------------------------------------
// <copyright file="FtpServer.cs" company="Fubar Development Junker">
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>
// <author>Mark Junker</author>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FubarDev.FtpServer.AccountManagement;
using FubarDev.FtpServer.CommandExtensions;
using FubarDev.FtpServer.CommandHandlers;
using FubarDev.FtpServer.FileSystem;
using FubarDev.FtpServer.Utilities;
using JetBrains.Annotations;
using Sockets.Plugin;
using Sockets.Plugin.Abstractions;
namespace FubarDev.FtpServer
{
/// <summary>
/// The portable FTP server
/// </summary>
public sealed class FtpServer : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly ConcurrentHashSet<FtpConnection> _connections = new ConcurrentHashSet<FtpConnection>();
private bool _stopped;
private ConfiguredTaskAwaitable _listenerTask;
private IFtpLog _log;
private Queue<int> _pasvPorts;
/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
/// </summary>
/// <param name="fileSystemClassFactory">The <see cref="IFileSystemClassFactory"/> to use to create the <see cref="IUnixFileSystem"/> for the logged in user.</param>
/// <param name="membershipProvider">The <see cref="IMembershipProvider"/> used to validate a login attempt</param>
/// <param name="commsInterface">The <see cref="ICommsInterface"/> that identifies the public IP address (required for <code>PASV</code> and <code>EPSV</code>)</param>
public FtpServer([NotNull] IFileSystemClassFactory fileSystemClassFactory, [NotNull] IMembershipProvider membershipProvider, [NotNull] ICommsInterface commsInterface)
: this(fileSystemClassFactory, membershipProvider, commsInterface, 21, new AssemblyFtpCommandHandlerFactory(typeof(FtpServer).GetTypeInfo().Assembly))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
/// </summary>
/// <param name="fileSystemClassFactory">The <see cref="IFileSystemClassFactory"/> to use to create the <see cref="IUnixFileSystem"/> for the logged in user.</param>
/// <param name="membershipProvider">The <see cref="IMembershipProvider"/> used to validate a login attempt</param>
/// <param name="serverAddress">The public IP address (required for <code>PASV</code> and <code>EPSV</code>)</param>
public FtpServer([NotNull] IFileSystemClassFactory fileSystemClassFactory, [NotNull] IMembershipProvider membershipProvider, [NotNull] string serverAddress)
: this(fileSystemClassFactory, membershipProvider, serverAddress, 21, null, new AssemblyFtpCommandHandlerFactory(typeof(FtpServer).GetTypeInfo().Assembly))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
/// </summary>
/// <param name="fileSystemClassFactory">The <see cref="IFileSystemClassFactory"/> to use to create the <see cref="IUnixFileSystem"/> for the logged in user.</param>
/// <param name="membershipProvider">The <see cref="IMembershipProvider"/> used to validate a login attempt</param>
/// <param name="commsInterface">The <see cref="ICommsInterface"/> that identifies the public IP address (required for <code>PASV</code> and <code>EPSV</code>)</param>
/// <param name="port">The port of the FTP server (usually 21)</param>
/// <param name="handlerFactory">The handler factories to create <see cref="FtpCommandHandler"/> and <see cref="FtpCommandHandlerExtension"/> instances for new <see cref="FtpConnection"/> objects</param>
public FtpServer([NotNull] IFileSystemClassFactory fileSystemClassFactory, [NotNull] IMembershipProvider membershipProvider, [NotNull] ICommsInterface commsInterface, int port, [NotNull, ItemNotNull] IFtpCommandHandlerFactory handlerFactory)
: this(fileSystemClassFactory, membershipProvider, commsInterface.IpAddress, port, null, handlerFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
/// </summary>
/// <param name="fileSystemClassFactory">The <see cref="IFileSystemClassFactory"/> to use to create the <see cref="IUnixFileSystem"/> for the logged in user.</param>
/// <param name="membershipProvider">The <see cref="IMembershipProvider"/> used to validate a login attempt</param>
/// <param name="serverAddress">The public IP address (required for <code>PASV</code> and <code>EPSV</code>)</param>
/// <param name="port">The port of the FTP server (usually 21)</param>
/// <param name="pasvPorts">The passive port of the FTP server when prefixed (ie constrained server)</param>
/// <param name="handlerFactory">The handler factories to create <see cref="FtpCommandHandler"/> and <see cref="FtpCommandHandlerExtension"/> instances for new <see cref="FtpConnection"/> objects</param>
public FtpServer([NotNull] IFileSystemClassFactory fileSystemClassFactory, [NotNull] IMembershipProvider membershipProvider, [NotNull] string serverAddress, int port, IEnumerable<int> pasvPorts, [NotNull, ItemNotNull] IFtpCommandHandlerFactory handlerFactory)
{
ServerAddress = serverAddress;
DefaultEncoding = Encoding.UTF8;
OperatingSystem = "UNIX";
FileSystemClassFactory = fileSystemClassFactory;
MembershipProvider = membershipProvider;
Port = port;
CommandsHandlerFactory = handlerFactory;
BackgroundTransferWorker = new BackgroundTransferWorker(this);
BackgroundTransferWorker.Start(_cancellationTokenSource);
_pasvPorts = pasvPorts == null ? null : new Queue<int>(pasvPorts);
}
/// <summary>
/// This event is raised when the connection is ready to be configured
/// </summary>
public event EventHandler<ConnectionEventArgs> ConfigureConnection;
/// <summary>
/// Gets or sets the returned operating system (default: UNIX)
/// </summary>
[NotNull]
public string OperatingSystem { get; set; }
/// <summary>
/// Gets the FTP server statistics
/// </summary>
[NotNull]
public FtpServerStatistics Statistics { get; } = new FtpServerStatistics();
/// <summary>
/// Gets the list of <see cref="IFtpCommandHandlerFactory"/> implementations to
/// create <see cref="FtpCommandHandler"/> instances for new <see cref="FtpConnection"/> objects.
/// </summary>
[NotNull]
public IFtpCommandHandlerFactory CommandsHandlerFactory { get; }
/// <summary>
/// Gets the public IP address (required for <code>PASV</code> and <code>EPSV</code>)
/// </summary>
[NotNull]
public string ServerAddress { get; }
/// <summary>
/// Gets the port on which the FTP server is listening for incoming connections
/// </summary>
public int Port { get; }
/// <summary>
/// Gets or sets the default text encoding for textual data
/// </summary>
[NotNull]
public Encoding DefaultEncoding { get; set; }
/// <summary>
/// Gets the <see cref="IFileSystemClassFactory"/> to use to create the <see cref="IUnixFileSystem"/> for the logged-in user.
/// </summary>
[NotNull]
public IFileSystemClassFactory FileSystemClassFactory { get; }
/// <summary>
/// Gets the <see cref="IMembershipProvider"/> used to validate a login attempt
/// </summary>
[NotNull]
public IMembershipProvider MembershipProvider { get; }
/// <summary>
/// Gets or sets the login manager used to create new <see cref="IFtpLog"/> objects.
/// </summary>
[CanBeNull]
public IFtpLogManager LogManager { get; set; }
private BackgroundTransferWorker BackgroundTransferWorker { get; }
/// <summary>
/// Starts the FTP server in the background
/// </summary>
public void Start()
{
if (_stopped)
throw new InvalidOperationException("Cannot start a previously stopped FTP server");
_listenerTask = ExecuteServerListener(_cancellationTokenSource.Token).ConfigureAwait(false);
}
/// <summary>
/// Stops the FTP server
/// </summary>
/// <remarks>
/// The FTP server cannot be started again after it was stopped.
/// </remarks>
public void Stop()
{
_cancellationTokenSource.Cancel(true);
_stopped = true;
}
/// <summary>
/// Get the background transfer states for all active <see cref="IBackgroundTransfer"/> objects.
/// </summary>
/// <returns>The background transfer states for all active <see cref="IBackgroundTransfer"/> objects</returns>
[NotNull]
[ItemNotNull]
public IReadOnlyCollection<Tuple<string, BackgroundTransferStatus>> GetBackgroundTaskStates()
{
return BackgroundTransferWorker.GetStates();
}
/// <summary>
/// Enqueue a new <see cref="IBackgroundTransfer"/> for the given <paramref name="connection"/>
/// </summary>
/// <param name="backgroundTransfer">The background transfer to enqueue</param>
/// <param name="connection">The connection to enqueue the background transfer for</param>
public void EnqueueBackgroundTransfer([NotNull] IBackgroundTransfer backgroundTransfer, [CanBeNull] FtpConnection connection)
{
var entry = new BackgroundTransferEntry(backgroundTransfer, connection?.Log);
BackgroundTransferWorker.Enqueue(entry);
}
/// <inheritdoc/>
public void Dispose()
{
if (!_stopped)
Stop();
try
{
_listenerTask.GetAwaiter().GetResult();
}
catch (TaskCanceledException)
{
// Ignorieren - alles ist OK
}
BackgroundTransferWorker.Dispose();
_cancellationTokenSource.Dispose();
_connections.Dispose();
}
internal int PeekPasvPort(TimeSpan timeout)
{
if (_pasvPorts == null)
return 0;
lock (_pasvPorts)
{
if (_pasvPorts.Count == 0)
{
do
{
if (Monitor.Wait(_pasvPorts, timeout) == false)
return -1;
} while (_pasvPorts.Count == 0);
}
return _pasvPorts.Dequeue();
}
}
internal void PushPasvPort(int port)
{
if (_pasvPorts != null)
{
lock (_pasvPorts)
{
_pasvPorts.Enqueue(port);
if (_pasvPorts.Count == 1 )
{
// wake up any blocked dequeue
Monitor.PulseAll(_pasvPorts);
}
}
}
}
private void OnConfigureConnection(FtpConnection connection)
{
ConfigureConnection?.Invoke(this, new ConnectionEventArgs(connection));
}
private async Task ExecuteServerListener(CancellationToken cancellationToken)
{
_log = LogManager?.CreateLog(typeof(FtpServer));
using (var listener = new TcpSocketListener(0))
{
listener.ConnectionReceived = ConnectionReceived;
try
{
await listener.StartListeningAsync(Port);
try
{
for (; ;)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
}
}
finally
{
await listener.StopListeningAsync();
foreach (var connection in _connections.ToArray())
connection.Close();
}
}
catch (Exception ex)
{
_log?.Fatal(ex, "{0}", ex.Message);
}
}
}
private void ConnectionReceived(object sender, TcpSocketListenerConnectEventArgs args)
{
var connection = new FtpConnection(this, args.SocketClient, DefaultEncoding);
Statistics.ActiveConnections += 1;
Statistics.TotalConnections += 1;
connection.Closed += ConnectionOnClosed;
_connections.Add(connection);
connection.Log = LogManager?.CreateLog(connection);
OnConfigureConnection(connection);
connection.Start();
}
private void ConnectionOnClosed(object sender, EventArgs eventArgs)
{
if (_stopped)
return;
var connection = (FtpConnection)sender;
_connections.Remove(connection);
Statistics.ActiveConnections -= 1;
}
}
}
//-----------------------------------------------------------------------
// <copyright file="PasvCommandHandler.cs" company="Fubar Development Junker">
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>
// <author>Mark Junker</author>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Sockets.Plugin;
namespace FubarDev.FtpServer.CommandHandlers
{
/// <summary>
/// The command handler for the <code>PASV</code> (4.1.2.) and <code>EPSV</code> commands
/// </summary>
public class PasvCommandHandler : FtpCommandHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="PasvCommandHandler"/> class.
/// </summary>
/// <param name="connection">The connection this command handler is created for</param>
public PasvCommandHandler(FtpConnection connection)
: base(connection, "PASV", "EPSV")
{
}
/// <inheritdoc/>
public override IEnumerable<IFeatureInfo> GetSupportedFeatures()
{
yield return new GenericFeatureInfo("EPSV");
}
/// <inheritdoc/>
public override async Task<FtpResponse> Process(FtpCommand command, CancellationToken cancellationToken)
{
if (Data.PassiveSocketClient != null)
{
await Data.PassiveSocketClient.DisconnectAsync();
Data.PassiveSocketClient.Dispose();
Data.PassiveSocketClient = null;
}
if (Data.TransferTypeCommandUsed != null && !string.Equals(command.Name, Data.TransferTypeCommandUsed, StringComparison.OrdinalIgnoreCase))
return new FtpResponse(500, $"Cannot use {command.Name} when {Data.TransferTypeCommandUsed} was used before.");
int port;
bool peeked = true ;
var isEpsv = string.Equals(command.Name, "EPSV", StringComparison.OrdinalIgnoreCase);
if (isEpsv)
{
if (string.IsNullOrEmpty(command.Argument) || string.Equals(command.Argument, "ALL", StringComparison.OrdinalIgnoreCase))
{
// peek the first available port - wait 5 second. return -1 if no port available
port = Server.PeekPasvPort(TimeSpan.FromSeconds(5));
}
else
{
port = Convert.ToInt32(command.Argument, 10);
peeked = false;
}
}
else
{
// peek the first available port -wait 5 second. return -1 if no port available
port = Server.PeekPasvPort(TimeSpan.FromSeconds(5));
}
Data.TransferTypeCommandUsed = command.Name;
if (port < 0)
{
// we do not found any available port
return new FtpResponse(425, $"Can't open data connection.");
}
var sem = new SemaphoreSlim(0, 1);
var listener = new TcpSocketListener();
listener.ConnectionReceived += (sender, args) =>
{
Data.PassiveSocketClient = args.SocketClient;
sem.Release();
};
await listener.StartListeningAsync(port);
var localPort = listener.LocalPort;
if (isEpsv || Server.ServerAddress.Contains(":"))
{
var listenerAddress = new Address(localPort);
await Connection.WriteAsync(new FtpResponse(229, $"Entering Extended Passive Mode ({listenerAddress})."), cancellationToken);
}
else
{
var listenerAddress = new Address(Server.ServerAddress, localPort);
await Connection.WriteAsync(new FtpResponse(227, $"Entering Passive Mode ({listenerAddress})."), cancellationToken);
}
await sem.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
await listener.StopListeningAsync();
listener.Dispose();
if (peeked)
{
// set the port as available.
Server.PushPasvPort(port);
}
return null;
}
}
}
////#define USE_FTPS_IMPLICIT
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using FubarDev.FtpServer;
using FubarDev.FtpServer.AccountManagement;
using FubarDev.FtpServer.AccountManagement.Anonymous;
using FubarDev.FtpServer.AuthTls;
using FubarDev.FtpServer.FileSystem.DotNet;
using TestFtpServer.Logging;
using System.Collections.Generic;
using System.Configuration;
namespace TestFtpServer
{
class Program
{
#if USE_FTPS_IMPLICIT
const int defaultPort = 990;
#else
const int defaultPort = 21;
#endif
const string portKey = "port";
const string portListKey = "pasvPorts";
const char portListSep = ',';
private static void Main()
{
// Load server certificate
var cert = new X509Certificate2("test.pfx");
AuthTlsCommandHandler.ServerCertificate = cert;
// Only allow anonymous login
var membershipProvider = new AnonymousMembershipProvider(new NoValidation());
// Use the .NET file system
var fsProvider = new DotNetFileSystemProvider(Path.Combine(Path.GetTempPath(), "TestFtpServer"));
// Use all commands from the FtpServer assembly and the one(s) from the AuthTls assembly
var commandFactory = new AssemblyFtpCommandHandlerFactory(typeof(FtpServer).Assembly, typeof(AuthTlsCommandHandler).Assembly);
// Set the ports using App Config
IEnumerable<int> pasvPort = null;
string str = ConfigurationManager.AppSettings[portListKey];
if( str != null )
{
pasvPort = str.ParsePorts(portListSep);
}
int Port = defaultPort;
str = ConfigurationManager.AppSettings[portKey];
if (str != null)
{
int tmp;
if (int.TryParse(str,out tmp))
{
Port = tmp;
}
}
// Initialize the FTP server
using (var ftpServer = new FtpServer(fsProvider, membershipProvider, "127.0.0.1", Port, pasvPort, commandFactory)
{
DefaultEncoding = Encoding.ASCII,
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
// Create the default logger
var log = ftpServer.LogManager != null ? ftpServer.LogManager.CreateLog(typeof(Program)) : null ;
try
{
// Start the FTP server
ftpServer.Start();
Console.WriteLine("Press ENTER/RETURN to close the test application.");
Console.ReadLine();
// Stop the FTP server
ftpServer.Stop();
}
catch (Exception ex)
{
if( log!= null)log.Error(ex, "Error during main FTP server loop");
}
}
}
}
}
// <copyright file="StringExtensions.cs" company="Fubar Development Junker">
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>
using System;
using System.Globalization;
using JetBrains.Annotations;
using System.Collections.Generic;
namespace FubarDev.FtpServer
{
/// <summary>
/// Extension methods for <see cref="string"/>
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Try to parse a timestamp from the parameter <paramref name="timestamp"/>.
/// </summary>
/// <param name="timestamp">The time stamp to parse</param>
/// <param name="timezone">The time zone of the timestamp (must always be <code>UTC</code>)</param>
/// <param name="result">The parsed timestamp</param>
/// <returns><code>true</code> when timestamp and timezone were valid.</returns>
public static bool TryParseTimestamp([NotNull] this string timestamp, [NotNull] string timezone, out DateTimeOffset result)
{
if (timestamp.Length != 12 && timestamp.Length != 14)
return false;
if (timezone != "UTC")
return false;
var format = "yyyyMMddHHmm" + (timestamp.Length == 14 ? "ss" : string.Empty);
result = DateTimeOffset.ParseExact(timestamp, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
return true;
}
/// <summary>
/// Parse a list of int.
/// </summary>
/// <param name="ports">The list of int as ascii separated value </param>
/// <param name="sep">the separator</param>
/// <returns>The parsed list - ommit the invalid number </returns>
public static IEnumerable<int> ParsePorts([NotNull] this string ports, char sep)
{
int tmp;
foreach (string s in ports.Split(sep))
{
if (int.TryParse(s, out tmp))
{
yield return tmp;
}
}
}
[NotNull]
internal static string ChompFromEnd([NotNull] this string input, out string token)
{
var pos = input.LastIndexOf(' ');
if (pos == -1)
{
token = input;
return string.Empty;
}
var remaining = input.Substring(0, pos);
token = input.Substring(pos + 1);
return remaining;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment