Skip to content

Commit 318fb3c

Browse files
committed
Implementation of a connection timeout.
Fixes #84
1 parent 3832738 commit 318fb3c

File tree

10 files changed

+310
-30
lines changed

10 files changed

+310
-30
lines changed

samples/TestFtpServer/Configuration/FtpServerOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,15 @@ public class FtpServerOptions
3737
/// 0 (default) means no control over connection count.
3838
/// </remarks>
3939
public int? MaxActiveConnections { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the interval between checks for inactive connections.
43+
/// </summary>
44+
public int? ConnectionInactivityCheckInterval { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the timeout for inactive connections.
48+
/// </summary>
49+
public int? InactivityTimeout { get; set; }
4050
}
4151
}

samples/TestFtpServer/ServiceCollectionExtensions.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,34 @@ public static IServiceCollection AddFtpServices(
4949
this IServiceCollection services,
5050
FtpOptions options)
5151
{
52+
static TimeSpan? ToTomeSpan(int? seconds)
53+
{
54+
return seconds == null
55+
? (TimeSpan?)null
56+
: TimeSpan.FromSeconds(seconds.Value);
57+
}
58+
5259
services
5360
.Configure<AuthTlsOptions>(
5461
opt =>
5562
{
5663
opt.ServerCertificate = options.GetCertificate();
5764
opt.ImplicitFtps = options.Ftps.Implicit;
5865
})
59-
.Configure<FtpConnectionOptions>(opt => opt.DefaultEncoding = Encoding.ASCII)
66+
.Configure<FtpConnectionOptions>(
67+
opt =>
68+
{
69+
opt.DefaultEncoding = Encoding.ASCII;
70+
opt.InactivityTimeout = ToTomeSpan(options.Server.InactivityTimeout);
71+
})
6072
.Configure<FubarDev.FtpServer.FtpServerOptions>(
6173
opt =>
6274
{
6375
opt.ServerAddress = options.Server.Address;
6476
opt.Port = options.GetServerPort();
6577
opt.MaxActiveConnections = options.Server.MaxActiveConnections ?? 0;
78+
opt.ConnectionInactivityCheckInterval =
79+
ToTomeSpan(options.Server.ConnectionInactivityCheckInterval);
6680
})
6781
.Configure<PortCommandOptions>(
6882
opt =>

samples/TestFtpServer/appsettings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"useFtpDataPort": false,
3636
/* Set to the maximum number of connections. A value of 0 (default) means that the connections aren't limited. */
3737
"maxActiveConnections": null,
38+
/* Sets the interval between checks for expired connections in seconds. */
39+
"connectionInactivityCheckInterval": 60,
40+
/* Sets the inactivity timeout for connections in seconds. */
41+
"inactivityTimeout": 300,
3842
/* PASV/EPSV-specific options */
3943
"pasv": {
4044
/* PASV port range in the form "from:to" (inclusive) */
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// <copyright file="IFtpConnectionKeepAlive.cs" company="Fubar Development Junker">
2+
// Copyright (c) Fubar Development Junker. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
7+
namespace FubarDev.FtpServer
8+
{
9+
/// <summary>
10+
/// Interface to ensure that a connection keeps alive.
11+
/// </summary>
12+
public interface IFtpConnectionKeepAlive
13+
{
14+
/// <summary>
15+
/// Gets a value indicating whether the connection is still alive.
16+
/// </summary>
17+
bool IsAlive { get; }
18+
19+
/// <summary>
20+
/// Gets the time of last activity (UTC).
21+
/// </summary>
22+
DateTime LastActivityUtc { get; }
23+
24+
/// <summary>
25+
/// Gets or sets a value indicating whether a data transfer is active.
26+
/// </summary>
27+
bool IsInDataTransfer { get; set; }
28+
29+
/// <summary>
30+
/// Ensure that the connection keeps alive.
31+
/// </summary>
32+
void KeepAlive();
33+
}
34+
}

src/FubarDev.FtpServer/FtpConnection.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public sealed class FtpConnection : FtpConnectionContext, IFtpConnection
8787
/// </remarks>
8888
private readonly IFtpService _streamWriterService;
8989

90+
private readonly IFtpConnectionKeepAlive _keepAliveFeature;
91+
9092
private bool _connectionClosing;
9193

9294
private int _connectionClosed;
@@ -143,6 +145,7 @@ public FtpConnection(
143145

144146
_loggerScope = logger?.BeginScope(properties);
145147

148+
_keepAliveFeature = new FtpConnectionKeepAlive(options.Value.InactivityTimeout);
146149
_socket = socket;
147150
_connectionAccessor = connectionAccessor;
148151
_serverCommandExecutor = serverCommandExecutor;
@@ -186,6 +189,7 @@ public FtpConnection(
186189
parentFeatures.Set<ISecureConnectionFeature>(secureConnectionFeature);
187190
parentFeatures.Set<IServerCommandFeature>(new ServerCommandFeature(_serverCommandChannel));
188191
parentFeatures.Set<INetworkStreamFeature>(_networkStreamFeature);
192+
parentFeatures.Set<IFtpConnectionKeepAlive>(_keepAliveFeature);
189193

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

646650
while (commandReader.TryRead(out var command))
647651
{
652+
_keepAliveFeature.KeepAlive();
648653
_logger?.Command(command);
649654
var context = new FtpContext(command, _serverCommandChannel, this);
650655
await requestDelegate(context)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// <copyright file="FtpConnectionKeepAlive.cs" company="Fubar Development Junker">
2+
// Copyright (c) Fubar Development Junker. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
7+
namespace FubarDev.FtpServer
8+
{
9+
internal class FtpConnectionKeepAlive : IFtpConnectionKeepAlive
10+
{
11+
/// <summary>
12+
/// The lock to be acquired when the timeout information gets set or read.
13+
/// </summary>
14+
private readonly object _inactivityTimeoutLock = new object();
15+
16+
/// <summary>
17+
/// The timeout for the detection of inactivity.
18+
/// </summary>
19+
private readonly TimeSpan _inactivityTimeout;
20+
21+
/// <summary>
22+
/// The timestamp of the last activity on the connection.
23+
/// </summary>
24+
private DateTime _utcLastActiveTime;
25+
26+
/// <summary>
27+
/// The timestamp where the connection expires.
28+
/// </summary>
29+
private DateTime? _expirationTimeout;
30+
31+
/// <summary>
32+
/// Indicator if a data transfer is ongoing.
33+
/// </summary>
34+
private bool _isInDataTransfer;
35+
36+
public FtpConnectionKeepAlive(TimeSpan? inactivityTimeout)
37+
{
38+
_inactivityTimeout = inactivityTimeout ?? TimeSpan.MaxValue;
39+
UpdateLastActiveTime();
40+
}
41+
42+
/// <inheritdoc />
43+
public bool IsAlive
44+
{
45+
get
46+
{
47+
lock (_inactivityTimeoutLock)
48+
{
49+
if (_expirationTimeout == null)
50+
{
51+
return true;
52+
}
53+
54+
if (_isInDataTransfer)
55+
{
56+
UpdateLastActiveTime();
57+
return true;
58+
}
59+
60+
return DateTime.UtcNow <= _expirationTimeout.Value;
61+
}
62+
}
63+
}
64+
65+
/// <inheritdoc />
66+
public DateTime LastActivityUtc
67+
{
68+
get
69+
{
70+
lock (_inactivityTimeoutLock)
71+
{
72+
return _utcLastActiveTime;
73+
}
74+
}
75+
}
76+
77+
/// <inheritdoc />
78+
public bool IsInDataTransfer
79+
{
80+
get
81+
{
82+
lock (_inactivityTimeoutLock)
83+
{
84+
// Reset the expiration timeout while a data transfer is ongoing.
85+
if (_isInDataTransfer)
86+
{
87+
UpdateLastActiveTime();
88+
}
89+
90+
return _isInDataTransfer;
91+
}
92+
}
93+
set
94+
{
95+
lock (_inactivityTimeoutLock)
96+
{
97+
// Reset the expiration timeout when the data transfer status gets updated.
98+
UpdateLastActiveTime();
99+
_isInDataTransfer = value;
100+
}
101+
}
102+
}
103+
104+
/// <inheritdoc />
105+
public void KeepAlive()
106+
{
107+
lock (_inactivityTimeoutLock)
108+
{
109+
UpdateLastActiveTime();
110+
}
111+
}
112+
113+
private void UpdateLastActiveTime()
114+
{
115+
_utcLastActiveTime = DateTime.UtcNow;
116+
_expirationTimeout = (_inactivityTimeout == TimeSpan.MaxValue)
117+
? (DateTime?)null
118+
: _utcLastActiveTime.Add(_inactivityTimeout);
119+
}
120+
}
121+
}

src/FubarDev.FtpServer/FtpConnectionOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright (c) Fubar Development Junker. All rights reserved.
33
// </copyright>
44

5+
using System;
56
using System.Text;
67

78
namespace FubarDev.FtpServer
@@ -15,5 +16,10 @@ public class FtpConnectionOptions
1516
/// Gets or sets the default connection encoding.
1617
/// </summary>
1718
public Encoding DefaultEncoding { get; set; } = Encoding.ASCII;
19+
20+
/// <summary>
21+
/// Gets or sets the default connection inactivity timeout.
22+
/// </summary>
23+
public TimeSpan? InactivityTimeout { get; set; } = TimeSpan.FromMinutes(5);
1824
}
1925
}

src/FubarDev.FtpServer/FtpServer.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public sealed class FtpServer : IFtpServer, IDisposable
3939
private readonly ILogger<FtpServer>? _log;
4040
private readonly Task _clientReader;
4141
private readonly CancellationTokenSource _serverShutdown = new CancellationTokenSource();
42+
private readonly Timer? _connectionTimeoutChecker;
4243

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

7172
_clientReader = ReadClientsAsync(tcpClientChannel, _serverShutdown.Token);
73+
74+
if (serverOptions.Value.ConnectionInactivityCheckInterval is TimeSpan checkInterval)
75+
{
76+
_connectionTimeoutChecker = new Timer(
77+
_ => CloseExpiredConnections(),
78+
null,
79+
checkInterval,
80+
checkInterval);
81+
}
7282
}
7383

7484
/// <inheritdoc />
@@ -103,13 +113,28 @@ public void Dispose()
103113
StopAsync(CancellationToken.None).Wait();
104114
}
105115

116+
_connectionTimeoutChecker?.Dispose();
117+
106118
_serverShutdown.Dispose();
107119
foreach (var connectionInfo in _connections.Values)
108120
{
109121
connectionInfo.Scope.Dispose();
110122
}
111123
}
112124

125+
/// <summary>
126+
/// Returns all connections.
127+
/// </summary>
128+
/// <returns>The currently active connections.</returns>
129+
/// <remarks>
130+
/// The connection might be closed between calling this function and
131+
/// using/querying the connection by the client.
132+
/// </remarks>
133+
public IEnumerable<IFtpConnection> GetConnections()
134+
{
135+
return _connections.Keys.ToList();
136+
}
137+
113138
/// <inheritdoc />
114139
[Obsolete("Use IFtpServerHost.StartAsync instead.")]
115140
void IFtpServer.Start()
@@ -156,6 +181,39 @@ public async Task StopAsync(CancellationToken cancellationToken)
156181
await _clientReader.ConfigureAwait(false);
157182
}
158183

184+
/// <summary>
185+
/// Close expired FTP connections.
186+
/// </summary>
187+
/// <remarks>
188+
/// This will always happen when the FTP client is idle (without sending notifications) or
189+
/// when the client was disconnected due to an undetectable network error.
190+
/// </remarks>
191+
private void CloseExpiredConnections()
192+
{
193+
foreach (var connection in GetConnections())
194+
{
195+
try
196+
{
197+
var keepAliveFeature = connection.Features.Get<IFtpConnectionKeepAlive>();
198+
if (keepAliveFeature.IsAlive)
199+
{
200+
// Ignore connections that are still alive.
201+
continue;
202+
}
203+
204+
var serverCommandFeature = connection.Features.Get<IServerCommandFeature>();
205+
206+
// Just ignore a failed write operation. We'll try again later.
207+
serverCommandFeature.ServerCommandWriter.TryWrite(
208+
new CloseConnectionServerCommand());
209+
}
210+
catch
211+
{
212+
// Errors are most likely indicating a closed connection. Nothing to do here...
213+
}
214+
}
215+
}
216+
159217
private IEnumerable<ConnectionInitAsyncDelegate> OnConfigureConnection(IFtpConnection connection)
160218
{
161219
var eventArgs = new ConnectionEventArgs(connection);

src/FubarDev.FtpServer/FtpServerOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Copyright (c) Fubar Development Junker. All rights reserved.
33
// </copyright>
44

5+
using System;
6+
57
namespace FubarDev.FtpServer
68
{
79
/// <summary>
@@ -28,5 +30,10 @@ public class FtpServerOptions
2830
/// 0 (default) means no control over connection count.
2931
/// </remarks>
3032
public int MaxActiveConnections { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the interval between checks for inactive connections.
36+
/// </summary>
37+
public TimeSpan? ConnectionInactivityCheckInterval { get; set; } = TimeSpan.FromMinutes(1);
3138
}
3239
}

0 commit comments

Comments
 (0)