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 System.Collections.Generic;
using DearFTP.Utils;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using YamlDotNet.Serialization;
namespace DearFTP.Configurations
@@ -31,6 +32,7 @@ namespace DearFTP.Configurations
}
public ServerConfiguration Server { get; set; } = new ServerConfiguration();
public TlsConfiguration Tls { get; set; } = new TlsConfiguration();
public Share[] Shares { get; set; } = Array.Empty<Share>();
public User[] Users { get; set; } = Array.Empty<User>();
@@ -58,6 +60,49 @@ namespace DearFTP.Configurations
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;
}

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[]
{
new AuthCommand(),
new ClntCommand(),
new CwdCommand(),
new DeleteCommand(),
@@ -21,6 +22,8 @@ namespace DearFTP.Connection.Commands
new OptionsCommand(),
new ParentDirectoryCommand(),
new PassiveCommand(),
new ProtectionBufferSizeCommand(),
new ProtectionCommand(),
new PwdCommand(),
new QuitCommand(),
new RenameCommand(),
@@ -43,15 +46,21 @@ namespace DearFTP.Connection.Commands
}
var commandExecutor = Commands.FirstOrDefault(x => x.Aliases.Contains(command, StringComparer.OrdinalIgnoreCase));
var stream = session.FtpStream;
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;
}
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,
"Features:",
"AUTH TLS",
"MDTM",
"MLST",
"PASV",
"PBSZ",
"PROT",
"REST STREAM",
"SIZE",
"TVFS",

View File

@@ -22,6 +22,7 @@ namespace DearFTP.Connection.Commands
"ABOR",
"ALLO",
"APPE",
"AUTH",
"CDUP",
"CWD",
"DELE",
@@ -38,7 +39,9 @@ namespace DearFTP.Connection.Commands
"OPTS",
"PASS",
"PASV",
"PBSZ",
"PORT",
"PROT",
"PWD",
"QUIT",
"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;
if (!dataConnection.IsAvailable)
if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
@@ -40,6 +40,12 @@ namespace DearFTP.Connection.Commands
stream.Send(ResponseCode.FileStatusOK, "File coming.");
if (dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
int restartPosition = session.RestartPosition;
session.RestartPosition = 0;

View File

@@ -22,7 +22,7 @@ namespace DearFTP.Connection.Commands
{
var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable)
if (!dataConnection.IsTslProtected && !dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
@@ -52,6 +52,12 @@ namespace DearFTP.Connection.Commands
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");
dataConnection.Close();
@@ -59,7 +65,7 @@ namespace DearFTP.Connection.Commands
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))
{

View File

@@ -1,6 +1,9 @@
using System;
using DearFTP.Configurations;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Threading;
@@ -10,11 +13,12 @@ namespace DearFTP.Connection
{
class DataConnection
{
public const int Timeout = 10_000;
public const int Timeout = 100_000;
public TcpListener Listener { 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 bool IsAvailable
{
@@ -41,7 +45,7 @@ namespace DearFTP.Connection
public DataConnection()
{
IsTslProtected = false;
}
public void Create()
@@ -56,15 +60,36 @@ namespace DearFTP.Connection
Listener.Start();
}
public void AcceptClient()
public void AcceptClient(bool authenticateAfter = false)
{
_acceptTask = Listener.AcceptTcpClientAsync().ContinueWith(t =>
{
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()
{
Stream.Close();

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
@@ -10,18 +11,18 @@ namespace DearFTP.Connection
{
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()
{
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)
{
@@ -49,14 +50,14 @@ namespace DearFTP.Connection
{
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)
{
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)
@@ -74,7 +75,7 @@ namespace DearFTP.Connection
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>
ArgumentNotImplemented = 504,
/// <summary>
/// Current level of protection is insufficient, need TLS/SSL
/// </summary>
InsufficientProtection = 522,
/// <summary>
/// Not logged in.
/// </summary>
NotLoggedIn = 530,

View File

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

1070
DearFTP/Utils/OpenSslKey.cs Normal file

File diff suppressed because it is too large Load Diff