-
-
Notifications
You must be signed in to change notification settings - Fork 180
Expand file tree
/
Copy pathFtpServer.cs
More file actions
395 lines (342 loc) · 14.6 KB
/
FtpServer.cs
File metadata and controls
395 lines (342 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
//-----------------------------------------------------------------------
// <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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using FubarDev.FtpServer.ConnectionChecks;
using FubarDev.FtpServer.Features;
using FubarDev.FtpServer.Localization;
using FubarDev.FtpServer.ServerCommands;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace FubarDev.FtpServer
{
/// <summary>
/// The portable FTP server.
/// </summary>
public sealed class FtpServer : IFtpServer, IDisposable
{
private readonly FtpServerStatistics _statistics = new FtpServerStatistics();
private readonly IServiceProvider _serviceProvider;
private readonly List<IFtpConnectionConfigurator> _connectionConfigurators;
private readonly List<IFtpControlStreamAdapter> _controlStreamAdapters;
private readonly ConcurrentDictionary<IFtpConnection, FtpConnectionInfo> _connections = new ConcurrentDictionary<IFtpConnection, FtpConnectionInfo>();
private readonly IFtpListenerService _serverListener;
private readonly ILogger<FtpServer>? _log;
private readonly Task _clientReader;
private readonly Timer? _connectionTimeoutChecker;
/// <summary>
/// Initializes a new instance of the <see cref="FtpServer"/> class.
/// </summary>
/// <param name="serverOptions">The server options.</param>
/// <param name="serviceProvider">The service provider used to query services.</param>
/// <param name="controlStreamAdapters">Adapters for the control connection stream.</param>
/// <param name="connectionConfigurators">Configurators for FTP connections.</param>
/// <param name="ftpListenerService">Listener service for FTP connections.</param>
/// <param name="logger">The FTP server logger.</param>
public FtpServer(
IOptions<FtpServerOptions> serverOptions,
IServiceProvider serviceProvider,
IEnumerable<IFtpControlStreamAdapter> controlStreamAdapters,
IEnumerable<IFtpConnectionConfigurator> connectionConfigurators,
IFtpListenerService ftpListenerService,
ILogger<FtpServer>? logger = null)
{
_serviceProvider = serviceProvider;
_connectionConfigurators = connectionConfigurators.ToList();
_controlStreamAdapters = controlStreamAdapters.ToList();
_log = logger;
ServerAddress = serverOptions.Value.ServerAddress;
Port = serverOptions.Value.Port;
MaxActiveConnections = serverOptions.Value.MaxActiveConnections;
_serverListener = ftpListenerService;
_serverListener.ListenerStarted += (s, e) =>
{
Port = e.Port;
OnListenerStarted(e);
};
_clientReader = ReadClientsAsync(
_serverListener.Channel,
_serverListener.ListenerShutdown.Token);
if (serverOptions.Value.ConnectionInactivityCheckInterval is TimeSpan checkInterval)
{
_connectionTimeoutChecker = new Timer(
_ => CloseExpiredConnections(),
null,
checkInterval,
checkInterval);
}
}
/// <inheritdoc />
public event EventHandler<ConnectionEventArgs>? ConfigureConnection;
/// <inheritdoc />
public event EventHandler<ListenerStartedEventArgs>? ListenerStarted;
/// <inheritdoc />
public IFtpServerStatistics Statistics => _statistics;
/// <inheritdoc />
public string? ServerAddress { get; }
/// <inheritdoc />
public int Port { get; private set; }
/// <inheritdoc />
public int MaxActiveConnections { get; }
/// <inheritdoc />
public FtpServiceStatus Status => _serverListener.Status;
/// <inheritdoc />
public bool Ready => Status == FtpServiceStatus.Running;
/// <inheritdoc/>
public void Dispose()
{
if (Status != FtpServiceStatus.Stopped)
{
StopAsync(CancellationToken.None).Wait();
}
_connectionTimeoutChecker?.Dispose();
(_serverListener as IDisposable)?.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()
{
var host = _serviceProvider.GetRequiredService<IFtpServerHost>();
host.StartAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
}
/// <inheritdoc />
[Obsolete("Use IFtpServerHost.StopAsync instead.")]
void IFtpServer.Stop()
{
var host = _serviceProvider.GetRequiredService<IFtpServerHost>();
host.StopAsync(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
}
/// <inheritdoc />
public Task PauseAsync(CancellationToken cancellationToken)
{
return _serverListener.PauseAsync(cancellationToken);
}
/// <inheritdoc />
public Task ContinueAsync(CancellationToken cancellationToken)
{
return _serverListener.ContinueAsync(cancellationToken);
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await _serverListener.StartAsync(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
if (!_serverListener.ListenerShutdown.IsCancellationRequested)
{
_serverListener.ListenerShutdown.Cancel(true);
}
await _serverListener.StopAsync(cancellationToken).ConfigureAwait(false);
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<IFtpConnectionStatusCheck>();
var isAlive = keepAliveFeature.CheckIfAlive();
if (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);
ConfigureConnection?.Invoke(this, eventArgs);
return eventArgs.AsyncInitFunctions;
}
private async Task ReadClientsAsync(
ChannelReader<TcpClient> tcpClientReader,
CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var hasClient = await tcpClientReader.WaitToReadAsync(cancellationToken)
.ConfigureAwait(false);
if (!hasClient)
{
return;
}
while (tcpClientReader.TryRead(out var client))
{
await AddClientAsync(client)
.ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
var exception = ex;
while (exception is AggregateException aggregateException && aggregateException.InnerException != null)
{
exception = aggregateException.InnerException;
}
switch (exception)
{
case OperationCanceledException _:
// Cancelled
break;
default:
throw;
}
}
finally
{
_log?.LogDebug("Stopped accepting connections");
}
}
private async Task AddClientAsync(TcpClient client)
{
var scope = _serviceProvider.CreateScope();
try
{
Stream socketStream = client.GetStream();
foreach (var controlStreamAdapter in _controlStreamAdapters)
{
socketStream = await controlStreamAdapter.WrapAsync(socketStream, CancellationToken.None)
.ConfigureAwait(false);
}
// Initialize information about the socket
var socketAccessor = scope.ServiceProvider.GetRequiredService<TcpSocketClientAccessor>();
socketAccessor.TcpSocketClient = client;
socketAccessor.TcpSocketStream = socketStream;
// Create the connection
var connection = scope.ServiceProvider.GetRequiredService<IFtpConnection>();
var connectionAccessor = scope.ServiceProvider.GetRequiredService<IFtpConnectionAccessor>();
connectionAccessor.FtpConnection = connection;
// Remember connection
if (!_connections.TryAdd(connection, new FtpConnectionInfo(scope)))
{
_log?.LogCritical("A new scope was created, but the connection couldn't be added to the list");
client.Dispose();
scope.Dispose();
return;
}
// Send initial message
var serverCommandWriter = connection.Features.Get<IServerCommandFeature>().ServerCommandWriter;
var blockConnection = MaxActiveConnections != 0
&& _statistics.ActiveConnections >= MaxActiveConnections;
if (blockConnection)
{
// Send response
var response = new FtpResponse(421, "Too many users, server is full.");
await serverCommandWriter.WriteAsync(new SendResponseServerCommand(response))
.ConfigureAwait(false);
// Send close
await serverCommandWriter.WriteAsync(new CloseConnectionServerCommand())
.ConfigureAwait(false);
}
else
{
var serverMessages = scope.ServiceProvider.GetRequiredService<IFtpServerMessages>();
var response = new FtpResponseTextBlock(220, serverMessages.GetBannerMessage());
// Send initial response
await serverCommandWriter.WriteAsync(
new SendResponseServerCommand(response),
connection.CancellationToken)
.ConfigureAwait(false);
}
// Statistics
_statistics.AddConnection();
// Statistics and cleanup
connection.Closed += ConnectionOnClosed;
// Connection configuration by host
var asyncInitFunctions = OnConfigureConnection(connection);
foreach (var asyncInitFunction in asyncInitFunctions)
{
await asyncInitFunction(connection, CancellationToken.None)
.ConfigureAwait(false);
}
// Configure the connection
foreach (var connectionConfigurator in _connectionConfigurators)
{
await connectionConfigurator.Configure(connection, CancellationToken.None)
.ConfigureAwait(false);
}
// Start connection
await connection.StartAsync()
.ConfigureAwait(false);
}
catch (Exception ex)
{
scope.Dispose();
_log?.LogError(ex, "Failed to start the client connection: {ErrorMessage}", ex.Message);
}
}
private void ConnectionOnClosed(object? sender, EventArgs eventArgs)
{
var connection = (IFtpConnection)(sender ?? throw new InvalidOperationException("Missing sender information."));
if (!_connections.TryRemove(connection, out var info))
{
return;
}
info.Scope.Dispose();
_statistics.CloseConnection();
}
private void OnListenerStarted(ListenerStartedEventArgs e)
{
ListenerStarted?.Invoke(this, e);
}
private class FtpConnectionInfo
{
public FtpConnectionInfo(IServiceScope scope)
{
Scope = scope;
}
public IServiceScope Scope { get; }
}
}
}