Add SSL/TLS protocol support

This commit is contained in:
2019-07-20 12:52:41 +02:00
parent c78181a1de
commit 23dfbe8788
15 changed files with 1331 additions and 37 deletions

View File

@@ -1,8 +1,9 @@
using System; using DearFTP.Utils;
using System.Collections.Generic; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace DearFTP.Configurations namespace DearFTP.Configurations
@@ -31,6 +32,7 @@ namespace DearFTP.Configurations
} }
public ServerConfiguration Server { get; set; } = new ServerConfiguration(); public ServerConfiguration Server { get; set; } = new ServerConfiguration();
public TlsConfiguration Tls { get; set; } = new TlsConfiguration();
public Share[] Shares { get; set; } = Array.Empty<Share>(); public Share[] Shares { get; set; } = Array.Empty<Share>();
public User[] Users { get; set; } = Array.Empty<User>(); public User[] Users { get; set; } = Array.Empty<User>();
@@ -58,6 +60,49 @@ namespace DearFTP.Configurations
return false; return false;
} }
if (Tls.ForceTls && !Tls.AllowTls)
{
Console.WriteLine("Tls is forced but not allowed.");
return false;
}
if (Tls.AllowTls)
{
if (string.IsNullOrWhiteSpace(Tls.CertificatePath))
{
Console.WriteLine("Tls is activated, but no certificate is specified.");
return false;
}
else
{
try
{
var certificate = new X509Certificate2(Tls.CertificatePath);
if (!certificate.HasPrivateKey)
{
if (string.IsNullOrWhiteSpace(Tls.PrivateKeyPath))
{
Console.WriteLine("No private key loaded and no path is specified.");
return false;
}
var privateKeyBytes = OpenSslKey.DecodePkcs8PrivateKey(File.ReadAllText(Tls.PrivateKeyPath));
var privateKey = OpenSslKey.DecodePrivateKeyInfo(privateKeyBytes);
certificate = certificate.CopyWithPrivateKey(privateKey);
}
Tls.X509Certificate = certificate;
}
catch (Exception e)
{
Console.WriteLine($"Can't load certificate: {e.Message}");
return false;
}
}
}
return true; return true;
} }

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using YamlDotNet.Serialization;
namespace DearFTP.Configurations
{
class TlsConfiguration
{
public bool AllowTls { get; set; } = false;
public bool ForceTls { get; set; } = false;
public string CertificatePath { get; set; } = "";
public string PrivateKeyPath { get; set; } = "";
[YamlIgnore()]
public X509Certificate2 X509Certificate { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace DearFTP.Connection.Commands
{
class AuthCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"AUTH"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
string protocol = argument.ToUpper();
if (protocol != "TLS" && protocol != "TLS-C" && protocol != "SSL")
{
stream.Send(ResponseCode.ArgumentNotImplemented, "Invalid argument, expected 'TLS'");
return;
}
var tlsConfiguration = session.Configuration.Tls;
if (!tlsConfiguration.AllowTls)
{
session.LogError("Tls", "Client tried to use Tls but Tls is desactivated");
stream.Send(ResponseCode.NotImplemented, "Tls is not enabled on this server");
return;
}
stream.Send(ResponseCode.AcceptAuthenticationMechanism, "Tls activated.");
session.ActivateTls();
}
}
}

View File

@@ -9,6 +9,7 @@ namespace DearFTP.Connection.Commands
{ {
public ICommand[] Commands { get; } = new ICommand[] public ICommand[] Commands { get; } = new ICommand[]
{ {
new AuthCommand(),
new ClntCommand(), new ClntCommand(),
new CwdCommand(), new CwdCommand(),
new DeleteCommand(), new DeleteCommand(),
@@ -21,6 +22,8 @@ namespace DearFTP.Connection.Commands
new OptionsCommand(), new OptionsCommand(),
new ParentDirectoryCommand(), new ParentDirectoryCommand(),
new PassiveCommand(), new PassiveCommand(),
new ProtectionBufferSizeCommand(),
new ProtectionCommand(),
new PwdCommand(), new PwdCommand(),
new QuitCommand(), new QuitCommand(),
new RenameCommand(), new RenameCommand(),
@@ -43,15 +46,21 @@ namespace DearFTP.Connection.Commands
} }
var commandExecutor = Commands.FirstOrDefault(x => x.Aliases.Contains(command, StringComparer.OrdinalIgnoreCase)); var commandExecutor = Commands.FirstOrDefault(x => x.Aliases.Contains(command, StringComparer.OrdinalIgnoreCase));
var stream = session.FtpStream;
if (commandExecutor == null) if (commandExecutor == null)
{ {
session.FtpStream.Send(ResponseCode.NotImplemented, $"Command '{command}' not implemented or invalid"); stream.Send(ResponseCode.NotImplemented, $"Command '{command}' not implemented or invalid");
return; return;
} }
commandExecutor.Execute(session, session.FtpStream, command, argument); if (session.Configuration.Tls.ForceTls && !session.IsTlsProtected && command.ToUpper() != "AUTH")
{
stream.Send(ResponseCode.InsufficientProtection, "Not protected connection is not allowed on this server.");
return;
}
commandExecutor.Execute(session, stream, command, argument);
} }
} }
} }

View File

@@ -18,9 +18,12 @@ namespace DearFTP.Connection.Commands
( (
ResponseCode.SystemStatusOrHelpReply, ResponseCode.SystemStatusOrHelpReply,
"Features:", "Features:",
"AUTH TLS",
"MDTM", "MDTM",
"MLST", "MLST",
"PASV", "PASV",
"PBSZ",
"PROT",
"REST STREAM", "REST STREAM",
"SIZE", "SIZE",
"TVFS", "TVFS",

View File

@@ -22,6 +22,7 @@ namespace DearFTP.Connection.Commands
"ABOR", "ABOR",
"ALLO", "ALLO",
"APPE", "APPE",
"AUTH",
"CDUP", "CDUP",
"CWD", "CWD",
"DELE", "DELE",
@@ -38,7 +39,9 @@ namespace DearFTP.Connection.Commands
"OPTS", "OPTS",
"PASS", "PASS",
"PASV", "PASV",
"PBSZ",
"PORT", "PORT",
"PROT",
"PWD", "PWD",
"QUIT", "QUIT",
"REIN", "REIN",

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ProtectionBufferSizeCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"PBSZ"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (argument != "0")
{
stream.Send(ResponseCode.ArgumentsError, "Invalid argument, expected '0'");
return;
}
stream.Send(ResponseCode.OK, "Ok.");
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ProtectionCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"PROT"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
switch (argument.ToUpper())
{
case "C":
session.DataConnection.DesactivateTsl();
stream.Send(ResponseCode.OK, "Data protection cleared.");
break;
case "P":
session.DataConnection.ActivateTsl();
stream.Send(ResponseCode.OK, "Data protection set.");
break;
default:
stream.Send(ResponseCode.ArgumentsError, "Invalid argument.");
break;
}
}
}
}

View File

@@ -18,7 +18,7 @@ namespace DearFTP.Connection.Commands
{ {
var dataConnection = session.DataConnection; var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable) if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{ {
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return; return;
@@ -40,6 +40,12 @@ namespace DearFTP.Connection.Commands
stream.Send(ResponseCode.FileStatusOK, "File coming."); stream.Send(ResponseCode.FileStatusOK, "File coming.");
if (dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
int restartPosition = session.RestartPosition; int restartPosition = session.RestartPosition;
session.RestartPosition = 0; session.RestartPosition = 0;

View File

@@ -22,7 +22,7 @@ namespace DearFTP.Connection.Commands
{ {
var dataConnection = session.DataConnection; var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable) if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{ {
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated."); stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return; return;
@@ -52,6 +52,12 @@ namespace DearFTP.Connection.Commands
stream.Send(ResponseCode.FileStatusOK, "Waiting file."); stream.Send(ResponseCode.FileStatusOK, "Waiting file.");
if (dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
ReceiveFile(dataConnection.Stream, realPath, alias.ToUpper() == "APPE"); ReceiveFile(dataConnection.Stream, realPath, alias.ToUpper() == "APPE");
dataConnection.Close(); dataConnection.Close();
@@ -59,7 +65,7 @@ namespace DearFTP.Connection.Commands
stream.Send(ResponseCode.CloseDataConnection, "File received."); stream.Send(ResponseCode.CloseDataConnection, "File received.");
} }
private void ReceiveFile(NetworkStream stream, string path, bool append) private void ReceiveFile(Stream stream, string path, bool append)
{ {
using (var file = File.Open(path, append ? FileMode.Append : FileMode.Create)) using (var file = File.Open(path, append ? FileMode.Append : FileMode.Create))
{ {

View File

@@ -1,6 +1,9 @@
using System; using DearFTP.Configurations;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Net; using System.Net;
using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -10,11 +13,12 @@ namespace DearFTP.Connection
{ {
class DataConnection class DataConnection
{ {
public const int Timeout = 10_000; public const int Timeout = 100_000;
public TcpListener Listener { get; private set; } public TcpListener Listener { get; private set; }
public TcpClient Client { get; private set; } public TcpClient Client { get; private set; }
public NetworkStream Stream { get; private set; } public Stream Stream { get; private set; }
public bool IsTslProtected { get; private set; }
public int Port => ((IPEndPoint)Listener.LocalEndpoint).Port; public int Port => ((IPEndPoint)Listener.LocalEndpoint).Port;
public bool IsAvailable public bool IsAvailable
{ {
@@ -41,7 +45,7 @@ namespace DearFTP.Connection
public DataConnection() public DataConnection()
{ {
IsTslProtected = false;
} }
public void Create() public void Create()
@@ -56,15 +60,36 @@ namespace DearFTP.Connection
Listener.Start(); Listener.Start();
} }
public void AcceptClient() public void AcceptClient(bool authenticateAfter = false)
{ {
_acceptTask = Listener.AcceptTcpClientAsync().ContinueWith(t => _acceptTask = Listener.AcceptTcpClientAsync().ContinueWith(t =>
{ {
Client = t.Result; Client = t.Result;
Stream = Client.GetStream();
if (IsTslProtected)
{
var sslStream = new SslStream(Client.GetStream(), false);
sslStream.AuthenticateAsServer(FtpServer.Instance.Configuration.Tls.X509Certificate, false, true);
Stream = sslStream;
}
else
{
Stream = Client.GetStream();
}
}); });
} }
public void ActivateTsl()
{
IsTslProtected = true;
}
public void DesactivateTsl()
{
IsTslProtected = false;
}
public void Close() public void Close()
{ {
Stream.Close(); Stream.Close();

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
@@ -10,18 +11,18 @@ namespace DearFTP.Connection
{ {
public const int BUFFER_SIZE = 4096; public const int BUFFER_SIZE = 4096;
public NetworkStream NetworkStream { get; } public Stream Stream { get; }
public FtpStream(NetworkStream networkStream) public FtpStream(Stream stream)
{ {
NetworkStream = networkStream; Stream = stream;
} }
public (string command, string argument) Receive() public (string command, string argument) Receive()
{ {
var buffer = new byte[BUFFER_SIZE]; var buffer = new byte[BUFFER_SIZE];
int readBytes = NetworkStream.Read(buffer, 0, buffer.Length); int readBytes = Stream.Read(buffer, 0, buffer.Length);
if (readBytes == 0) if (readBytes == 0)
{ {
@@ -49,14 +50,14 @@ namespace DearFTP.Connection
{ {
var bytes = Encoding.UTF8.GetBytes($"{message}{(end ? "\r\n" : "")}"); var bytes = Encoding.UTF8.GetBytes($"{message}{(end ? "\r\n" : "")}");
NetworkStream.Write(bytes, 0, bytes.Length); Stream.Write(bytes, 0, bytes.Length);
} }
public void Send(ResponseCode code, string argument) public void Send(ResponseCode code, string argument)
{ {
var bytes = Encoding.UTF8.GetBytes($"{(uint)code} {argument}\r\n"); var bytes = Encoding.UTF8.GetBytes($"{(uint)code} {argument}\r\n");
NetworkStream.Write(bytes, 0, bytes.Length); Stream.Write(bytes, 0, bytes.Length);
} }
public void Send(ResponseCode code, string message, params string[] arguments) public void Send(ResponseCode code, string message, params string[] arguments)
@@ -74,7 +75,7 @@ namespace DearFTP.Connection
var bytes = Encoding.UTF8.GetBytes(builder.ToString()); var bytes = Encoding.UTF8.GetBytes(builder.ToString());
NetworkStream.Write(bytes, 0, bytes.Length); Stream.Write(bytes, 0, bytes.Length);
} }
} }
} }

View File

@@ -175,6 +175,10 @@ namespace DearFTP.Connection
/// </summary> /// </summary>
ArgumentNotImplemented = 504, ArgumentNotImplemented = 504,
/// <summary> /// <summary>
/// Current level of protection is insufficient, need TLS/SSL
/// </summary>
InsufficientProtection = 522,
/// <summary>
/// Not logged in. /// Not logged in.
/// </summary> /// </summary>
NotLoggedIn = 530, NotLoggedIn = 530,

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
@@ -20,14 +21,17 @@ namespace DearFTP.Connection
public Share[] Shares { get; set; } public Share[] Shares { get; set; }
public Share[] WritablesShares { get; set; } public Share[] WritablesShares { get; set; }
public NavigablePath NavigablePath { get; set; } public NavigablePath NavigablePath { get; set; }
public FtpStream FtpStream { get; } public FtpStream FtpStream { get; private set; }
public DataConnection DataConnection { get; set; } public DataConnection DataConnection { get; set; }
public int RestartPosition { get; set; } public int RestartPosition { get; set; }
public bool IsTlsProtected { get; private set; }
public string CurrentWorkingDirectory => NavigablePath.CurrentDirectory; public string CurrentWorkingDirectory => NavigablePath.CurrentDirectory;
public string IP => ((IPEndPoint)_client.Client.LocalEndPoint).Address.ToString(); public string IP => ((IPEndPoint)_client.Client.LocalEndPoint).Address.ToString();
private TcpClient _client; private TcpClient _client;
private NetworkStream _networkStream; private NetworkStream _networkStream;
private SslStream _sslStream;
private bool _isRunning = true; private bool _isRunning = true;
public Session(TcpClient client) public Session(TcpClient client)
@@ -41,6 +45,7 @@ namespace DearFTP.Connection
Logger = FtpServer.Instance.Logger; Logger = FtpServer.Instance.Logger;
DataConnection = new DataConnection(); DataConnection = new DataConnection();
RestartPosition = 0; RestartPosition = 0;
IsTlsProtected = false;
} }
public void Start() public void Start()
@@ -50,7 +55,7 @@ namespace DearFTP.Connection
while (_isRunning) while (_isRunning)
{ {
(string command, string argument) = FtpStream.Receive(); (string command, string argument) = FtpStream.Receive();
Logger.Log($"[{_client.Client.RemoteEndPoint}]: {command} {argument}"); Log($"{command} {argument}");
CommandsDispatcher.Dispatch(this, command, argument); CommandsDispatcher.Dispatch(this, command, argument);
} }
@@ -63,29 +68,30 @@ namespace DearFTP.Connection
Dispose(); Dispose();
} }
public string GetRealPath(string path) public void Log(string message)
{ {
string completePath = Path.Combine(CurrentWorkingDirectory, path); Logger.Log($"[{_client.Client.RemoteEndPoint}]: {message}");
}
string[] split = CurrentWorkingDirectory.Substring(1).Split('/'); public void LogError(string error, string description)
{
Logger.LogError(error, $"[{_client.Client.RemoteEndPoint}]: {description}");
}
if (split.Length == 0) public void ActivateTls()
{ {
return null; _sslStream = new SslStream(_networkStream, true);
}
var share = Shares.FirstOrDefault(x => x.Name == split[0]); _sslStream.AuthenticateAsServer(Configuration.Tls.X509Certificate, false, true);
if (share == null) FtpStream = new FtpStream(_sslStream);
{
return null;
}
return Path.Combine(split.Skip(1).Prepend(share.Path).ToArray()); IsTlsProtected = true;
} }
public void Dispose() public void Dispose()
{ {
_sslStream?.Close();
_networkStream.Close(); _networkStream.Close();
_client.Close(); _client.Close();
} }

1070
DearFTP/Utils/OpenSslKey.cs Normal file

File diff suppressed because it is too large Load Diff