Add SSL/TLS protocol support
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
19
DearFTP/Configurations/TlsConfiguration.cs
Normal file
19
DearFTP/Configurations/TlsConfiguration.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
40
DearFTP/Connection/Commands/AuthCommand.cs
Normal file
40
DearFTP/Connection/Commands/AuthCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ namespace DearFTP.Connection.Commands
|
||||
(
|
||||
ResponseCode.SystemStatusOrHelpReply,
|
||||
"Features:",
|
||||
"AUTH TLS",
|
||||
"MDTM",
|
||||
"MLST",
|
||||
"PASV",
|
||||
"PBSZ",
|
||||
"PROT",
|
||||
"REST STREAM",
|
||||
"SIZE",
|
||||
"TVFS",
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs
Normal file
25
DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
32
DearFTP/Connection/Commands/ProtectionCommand.cs
Normal file
32
DearFTP/Connection/Commands/ProtectionCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
1070
DearFTP/Utils/OpenSslKey.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user