Skip to content

Commit

Permalink
Implementation of a connection timeout.
Browse files Browse the repository at this point in the history
Fixes #84
  • Loading branch information
fubar-coder committed Nov 6, 2019
1 parent 3832738 commit 318fb3c
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 30 deletions.
10 changes: 10 additions & 0 deletions samples/TestFtpServer/Configuration/FtpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,15 @@ public class FtpServerOptions
/// 0 (default) means no control over connection count.
/// </remarks>
public int? MaxActiveConnections { get; set; }

/// <summary>
/// Gets or sets the interval between checks for inactive connections.
/// </summary>
public int? ConnectionInactivityCheckInterval { get; set; }

/// <summary>
/// Gets or sets the timeout for inactive connections.
/// </summary>
public int? InactivityTimeout { get; set; }
}
}
16 changes: 15 additions & 1 deletion samples/TestFtpServer/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,34 @@ public static IServiceCollection AddFtpServices(
this IServiceCollection services,
FtpOptions options)
{
static TimeSpan? ToTomeSpan(int? seconds)
{
return seconds == null
? (TimeSpan?)null
: TimeSpan.FromSeconds(seconds.Value);
}

services
.Configure<AuthTlsOptions>(
opt =>
{
opt.ServerCertificate = options.GetCertificate();
opt.ImplicitFtps = options.Ftps.Implicit;
})
.Configure<FtpConnectionOptions>(opt => opt.DefaultEncoding = Encoding.ASCII)
.Configure<FtpConnectionOptions>(
opt =>
{
opt.DefaultEncoding = Encoding.ASCII;
opt.InactivityTimeout = ToTomeSpan(options.Server.InactivityTimeout);
})
.Configure<FubarDev.FtpServer.FtpServerOptions>(
opt =>
{
opt.ServerAddress = options.Server.Address;
opt.Port = options.GetServerPort();
opt.MaxActiveConnections = options.Server.MaxActiveConnections ?? 0;
opt.ConnectionInactivityCheckInterval =
ToTomeSpan(options.Server.ConnectionInactivityCheckInterval);
})
.Configure<PortCommandOptions>(
opt =>
Expand Down
4 changes: 4 additions & 0 deletions samples/TestFtpServer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"useFtpDataPort": false,
/* Set to the maximum number of connections. A value of 0 (default) means that the connections aren't limited. */
"maxActiveConnections": null,
/* Sets the interval between checks for expired connections in seconds. */
"connectionInactivityCheckInterval": 60,
/* Sets the inactivity timeout for connections in seconds. */
"inactivityTimeout": 300,
/* PASV/EPSV-specific options */
"pasv": {
/* PASV port range in the form "from:to" (inclusive) */
Expand Down
34 changes: 34 additions & 0 deletions src/FubarDev.FtpServer.Abstractions/IFtpConnectionKeepAlive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// <copyright file="IFtpConnectionKeepAlive.cs" company="Fubar Development Junker">
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>

using System;

namespace FubarDev.FtpServer
{
/// <summary>
/// Interface to ensure that a connection keeps alive.
/// </summary>
public interface IFtpConnectionKeepAlive
{
/// <summary>
/// Gets a value indicating whether the connection is still alive.
/// </summary>
bool IsAlive { get; }

/// <summary>
/// Gets the time of last activity (UTC).
/// </summary>
DateTime LastActivityUtc { get; }

/// <summary>
/// Gets or sets a value indicating whether a data transfer is active.
/// </summary>
bool IsInDataTransfer { get; set; }

/// <summary>
/// Ensure that the connection keeps alive.
/// </summary>
void KeepAlive();
}
}
5 changes: 5 additions & 0 deletions src/FubarDev.FtpServer/FtpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public sealed class FtpConnection : FtpConnectionContext, IFtpConnection
/// </remarks>
private readonly IFtpService _streamWriterService;

private readonly IFtpConnectionKeepAlive _keepAliveFeature;

private bool _connectionClosing;

private int _connectionClosed;
Expand Down Expand Up @@ -143,6 +145,7 @@ public FtpConnection(

_loggerScope = logger?.BeginScope(properties);

_keepAliveFeature = new FtpConnectionKeepAlive(options.Value.InactivityTimeout);
_socket = socket;
_connectionAccessor = connectionAccessor;
_serverCommandExecutor = serverCommandExecutor;
Expand Down Expand Up @@ -186,6 +189,7 @@ public FtpConnection(
parentFeatures.Set<ISecureConnectionFeature>(secureConnectionFeature);
parentFeatures.Set<IServerCommandFeature>(new ServerCommandFeature(_serverCommandChannel));
parentFeatures.Set<INetworkStreamFeature>(_networkStreamFeature);
parentFeatures.Set<IFtpConnectionKeepAlive>(_keepAliveFeature);

var features = new FeatureCollection(parentFeatures);
#pragma warning disable 618
Expand Down Expand Up @@ -645,6 +649,7 @@ private async Task CommandChannelDispatcherAsync(ChannelReader<FtpCommand> comma

while (commandReader.TryRead(out var command))
{
_keepAliveFeature.KeepAlive();
_logger?.Command(command);
var context = new FtpContext(command, _serverCommandChannel, this);
await requestDelegate(context)
Expand Down
121 changes: 121 additions & 0 deletions src/FubarDev.FtpServer/FtpConnectionKeepAlive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// <copyright file="FtpConnectionKeepAlive.cs" company="Fubar Development Junker">
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>

using System;

namespace FubarDev.FtpServer
{
internal class FtpConnectionKeepAlive : IFtpConnectionKeepAlive
{
/// <summary>
/// The lock to be acquired when the timeout information gets set or read.
/// </summary>
private readonly object _inactivityTimeoutLock = new object();

/// <summary>
/// The timeout for the detection of inactivity.
/// </summary>
private readonly TimeSpan _inactivityTimeout;

/// <summary>
/// The timestamp of the last activity on the connection.
/// </summary>
private DateTime _utcLastActiveTime;

/// <summary>
/// The timestamp where the connection expires.
/// </summary>
private DateTime? _expirationTimeout;

/// <summary>
/// Indicator if a data transfer is ongoing.
/// </summary>
private bool _isInDataTransfer;

public FtpConnectionKeepAlive(TimeSpan? inactivityTimeout)
{
_inactivityTimeout = inactivityTimeout ?? TimeSpan.MaxValue;
UpdateLastActiveTime();
}

/// <inheritdoc />
public bool IsAlive
{
get
{
lock (_inactivityTimeoutLock)
{
if (_expirationTimeout == null)
{
return true;
}

if (_isInDataTransfer)
{
UpdateLastActiveTime();
return true;
}

return DateTime.UtcNow <= _expirationTimeout.Value;
}
}
}

/// <inheritdoc />
public DateTime LastActivityUtc
{
get
{
lock (_inactivityTimeoutLock)
{
return _utcLastActiveTime;
}
}
}

/// <inheritdoc />
public bool IsInDataTransfer
{
get
{
lock (_inactivityTimeoutLock)
{
// Reset the expiration timeout while a data transfer is ongoing.
if (_isInDataTransfer)
{
UpdateLastActiveTime();
}

return _isInDataTransfer;
}
}
set
{
lock (_inactivityTimeoutLock)
{
// Reset the expiration timeout when the data transfer status gets updated.
UpdateLastActiveTime();
_isInDataTransfer = value;
}
}
}

/// <inheritdoc />
public void KeepAlive()
{
lock (_inactivityTimeoutLock)
{
UpdateLastActiveTime();
}
}

private void UpdateLastActiveTime()
{
_utcLastActiveTime = DateTime.UtcNow;
_expirationTimeout = (_inactivityTimeout == TimeSpan.MaxValue)
? (DateTime?)null
: _utcLastActiveTime.Add(_inactivityTimeout);
}
}
}
6 changes: 6 additions & 0 deletions src/FubarDev.FtpServer/FtpConnectionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>

using System;
using System.Text;

namespace FubarDev.FtpServer
Expand All @@ -15,5 +16,10 @@ public class FtpConnectionOptions
/// Gets or sets the default connection encoding.
/// </summary>
public Encoding DefaultEncoding { get; set; } = Encoding.ASCII;

/// <summary>
/// Gets or sets the default connection inactivity timeout.
/// </summary>
public TimeSpan? InactivityTimeout { get; set; } = TimeSpan.FromMinutes(5);
}
}
58 changes: 58 additions & 0 deletions src/FubarDev.FtpServer/FtpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public sealed class FtpServer : IFtpServer, IDisposable
private readonly ILogger<FtpServer>? _log;
private readonly Task _clientReader;
private readonly CancellationTokenSource _serverShutdown = new CancellationTokenSource();
private readonly Timer? _connectionTimeoutChecker;

/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
Expand Down Expand Up @@ -69,6 +70,15 @@ public FtpServer(
};

_clientReader = ReadClientsAsync(tcpClientChannel, _serverShutdown.Token);

if (serverOptions.Value.ConnectionInactivityCheckInterval is TimeSpan checkInterval)
{
_connectionTimeoutChecker = new Timer(
_ => CloseExpiredConnections(),
null,
checkInterval,
checkInterval);
}
}

/// <inheritdoc />
Expand Down Expand Up @@ -103,13 +113,28 @@ public void Dispose()
StopAsync(CancellationToken.None).Wait();
}

_connectionTimeoutChecker?.Dispose();

_serverShutdown.Dispose();
foreach (var connectionInfo in _connections.Values)
{
connectionInfo.Scope.Dispose();
}
}

/// <summary>
/// Returns all connections.
/// </summary>
/// <returns>The currently active connections.</returns>
/// <remarks>
/// The connection might be closed between calling this function and
/// using/querying the connection by the client.
/// </remarks>
public IEnumerable<IFtpConnection> GetConnections()
{
return _connections.Keys.ToList();
}

/// <inheritdoc />
[Obsolete("Use IFtpServerHost.StartAsync instead.")]
void IFtpServer.Start()
Expand Down Expand Up @@ -156,6 +181,39 @@ public async Task StopAsync(CancellationToken cancellationToken)
await _clientReader.ConfigureAwait(false);
}

/// <summary>
/// Close expired FTP connections.
/// </summary>
/// <remarks>
/// This will always happen when the FTP client is idle (without sending notifications) or
/// when the client was disconnected due to an undetectable network error.
/// </remarks>
private void CloseExpiredConnections()
{
foreach (var connection in GetConnections())
{
try
{
var keepAliveFeature = connection.Features.Get<IFtpConnectionKeepAlive>();
if (keepAliveFeature.IsAlive)
{
// Ignore connections that are still alive.
continue;
}

var serverCommandFeature = connection.Features.Get<IServerCommandFeature>();

// Just ignore a failed write operation. We'll try again later.
serverCommandFeature.ServerCommandWriter.TryWrite(
new CloseConnectionServerCommand());
}
catch
{
// Errors are most likely indicating a closed connection. Nothing to do here...
}
}
}

private IEnumerable<ConnectionInitAsyncDelegate> OnConfigureConnection(IFtpConnection connection)
{
var eventArgs = new ConnectionEventArgs(connection);
Expand Down
7 changes: 7 additions & 0 deletions src/FubarDev.FtpServer/FtpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Copyright (c) Fubar Development Junker. All rights reserved.
// </copyright>

using System;

namespace FubarDev.FtpServer
{
/// <summary>
Expand All @@ -28,5 +30,10 @@ public class FtpServerOptions
/// 0 (default) means no control over connection count.
/// </remarks>
public int MaxActiveConnections { get; set; }

/// <summary>
/// Gets or sets the interval between checks for inactive connections.
/// </summary>
public TimeSpan? ConnectionInactivityCheckInterval { get; set; } = TimeSpan.FromMinutes(1);
}
}
Loading

0 comments on commit 318fb3c

Please sign in to comment.