Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f81d09d540 | |||
| 351030ebcd | |||
| bf42471fe5 | |||
| a28bff79fb | |||
| 594e7eb1dd | |||
| e8baee2d1e | |||
| 2d517ea090 | |||
| 3f80eef90b | |||
| d77bff4531 | |||
| eb0d9c8847 | |||
| e174f81fcc | |||
| 5a03dbf2a3 | |||
| 94431aebb8 | |||
| ffb62b7737 | |||
| f47100e940 | |||
| bbd83492a9 | |||
| 01fbd0b74e | |||
| b2189e63df | |||
| 23dfbe8788 | |||
| c78181a1de | |||
| f39dd7b764 | |||
| bac6728971 | |||
| 8c2b32d6b1 | |||
| a2f867480a | |||
| 8ce87a3cfc | |||
| 5face5379c |
@@ -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.Net;
|
||||
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>();
|
||||
|
||||
@@ -41,6 +43,16 @@ namespace DearFTP.Configurations
|
||||
|
||||
public bool Check()
|
||||
{
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Server.Host))
|
||||
{
|
||||
Console.WriteLine("Host address is missing.");
|
||||
}
|
||||
if (!IPAddress.TryParse(Server.Host, out var _))
|
||||
{
|
||||
Server.Host = Dns.GetHostEntry(Server.Host).AddressList.First().ToString();
|
||||
}
|
||||
|
||||
foreach (var share in Shares)
|
||||
{
|
||||
share.Name = Path.GetFileName(share.Path);
|
||||
@@ -58,12 +70,61 @@ namespace DearFTP.Configurations
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Tls.ForceTls && !Tls.AllowTls)
|
||||
{
|
||||
Console.WriteLine("Tls is forced but not allowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Server.ImplicitFtpsPort != 0 && !Tls.AllowTls)
|
||||
{
|
||||
Console.WriteLine("Implicit FTPS is activated but TLS is 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;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var serializer = new SerializerBuilder().EmitDefaults().Build();
|
||||
var serializer = new SerializerBuilder().Build();
|
||||
|
||||
File.WriteAllText(ConfigurationPath, serializer.Serialize(this));
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Configurations
|
||||
namespace DearFTP.Configurations
|
||||
{
|
||||
class ServerConfiguration
|
||||
{
|
||||
public string Host { get; set; } = "127.0.0.1";
|
||||
public ushort Port { get; set; } = 21;
|
||||
public ushort ForceDataPort { get; set; } = 0;
|
||||
public ushort ImplicitFtpsPort { get; set; } = 0;
|
||||
public string MOTD { get; set; } = "DearFTP v0.1";
|
||||
public string LoginMessage { get; set; } = "Logged in as %user%";
|
||||
|
||||
@@ -14,5 +13,6 @@ namespace DearFTP.Configurations
|
||||
public string LogFilePath { get; set; } = "log.txt";
|
||||
|
||||
public string Guest { get; set; } = "";
|
||||
public bool BypassGuestPassword { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace DearFTP.Configurations
|
||||
{
|
||||
|
||||
16
DearFTP/Configurations/TlsConfiguration.cs
Normal file
16
DearFTP/Configurations/TlsConfiguration.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Configurations
|
||||
namespace DearFTP.Configurations
|
||||
{
|
||||
class User
|
||||
{
|
||||
|
||||
21
DearFTP/Connection/Commands/AbortCommand.cs
Normal file
21
DearFTP/Connection/Commands/AbortCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class AbortCommand : ICommand
|
||||
{
|
||||
public string[] Aliases { get; } = new string[]
|
||||
{
|
||||
"ABOR"
|
||||
};
|
||||
|
||||
public void Execute(Session session, FtpStream stream, string alias, string argument)
|
||||
{
|
||||
session.DataConnection.Close();
|
||||
|
||||
stream.Send(ResponseCode.CloseDataConnection, "Data connection closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
34
DearFTP/Connection/Commands/AuthCommand.cs
Normal file
34
DearFTP/Connection/Commands/AuthCommand.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class ClntCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -9,21 +7,28 @@ namespace DearFTP.Connection.Commands
|
||||
{
|
||||
public ICommand[] Commands { get; } = new ICommand[]
|
||||
{
|
||||
new AbortCommand(),
|
||||
new AuthCommand(),
|
||||
new ClntCommand(),
|
||||
new CwdCommand(),
|
||||
new DeleteCommand(),
|
||||
new ExtendedPassiveCommand(),
|
||||
new HelpCommand(),
|
||||
new FeaturesCommand(),
|
||||
new FileModificationTimeCommand(),
|
||||
new ListCommand(),
|
||||
new ListMachineCommand(),
|
||||
new MakeDirectoryCommand(),
|
||||
new OptionsCommand(),
|
||||
new ParentDirectoryCommand(),
|
||||
new PassiveCommand(),
|
||||
new ProtectionBufferSizeCommand(),
|
||||
new ProtectionCommand(),
|
||||
new PwdCommand(),
|
||||
new QuitCommand(),
|
||||
new RenameCommand(),
|
||||
new RetrieveCommand(),
|
||||
new RestartCommand(),
|
||||
new SiteCommand(),
|
||||
new SizeCommand(),
|
||||
new StoreCommand(),
|
||||
@@ -41,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class CwdCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using DearFTP.Configurations;
|
||||
using DearFTP.Utils;
|
||||
using DearFTP.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
|
||||
26
DearFTP/Connection/Commands/ExtendedPassiveCommand.cs
Normal file
26
DearFTP/Connection/Commands/ExtendedPassiveCommand.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class ExtendedPassiveCommand : ICommand
|
||||
{
|
||||
public string[] Aliases { get; } = new string[]
|
||||
{
|
||||
"EPSV"
|
||||
};
|
||||
|
||||
public void Execute(Session session, FtpStream stream, string alias, string argument)
|
||||
{
|
||||
session.DataConnection.Create();
|
||||
|
||||
int port = session.DataConnection.Port;
|
||||
|
||||
stream.Send(ResponseCode.ExtendedPassiveMode, $"Entering Extended Passive Mode (|||{port}|)");
|
||||
|
||||
session.DataConnection.AcceptClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class FeaturesCommand : ICommand
|
||||
{
|
||||
@@ -18,9 +14,12 @@ namespace DearFTP.Connection.Commands
|
||||
(
|
||||
ResponseCode.SystemStatusOrHelpReply,
|
||||
"Features:",
|
||||
"AUTH TLS",
|
||||
"MDTM",
|
||||
"MLST",
|
||||
"PASV",
|
||||
"PBSZ",
|
||||
"PROT",
|
||||
"REST STREAM",
|
||||
"SIZE",
|
||||
"TVFS",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class HelpCommand : ICommand
|
||||
{
|
||||
@@ -22,9 +18,11 @@ namespace DearFTP.Connection.Commands
|
||||
"ABOR",
|
||||
"ALLO",
|
||||
"APPE",
|
||||
"AUTH",
|
||||
"CDUP",
|
||||
"CWD",
|
||||
"DELE",
|
||||
"EPSV",
|
||||
"FEAT",
|
||||
"HELP",
|
||||
"LIST",
|
||||
@@ -38,7 +36,9 @@ namespace DearFTP.Connection.Commands
|
||||
"OPTS",
|
||||
"PASS",
|
||||
"PASV",
|
||||
"PBSZ",
|
||||
"PORT",
|
||||
"PROT",
|
||||
"PWD",
|
||||
"QUIT",
|
||||
"REIN",
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
interface ICommand
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
@@ -31,6 +32,8 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
stream.Send(ResponseCode.FileStatusOK, "Listing coming.");
|
||||
|
||||
dataConnection.Authenticate();
|
||||
|
||||
string path = null;
|
||||
bool humanReadable = false;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Net.Security;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -72,6 +72,8 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
stream.Send(ResponseCode.FileStatusOK, "Listing coming.");
|
||||
|
||||
dataConnection.Authenticate();
|
||||
|
||||
var dataStream = new FtpStream(dataConnection.Stream);
|
||||
|
||||
foreach (var info in infos)
|
||||
@@ -114,7 +116,7 @@ namespace DearFTP.Connection.Commands
|
||||
{
|
||||
string currentDirectory = navigablePath.GetDirectoryPath("");
|
||||
|
||||
yield return GenerateInfo(new DirectoryInfo(currentDirectory), writeable, "cdir");
|
||||
yield return GenerateInfo(new DirectoryInfo(currentDirectory), writeable, "cdir");
|
||||
|
||||
if (virtualDirectory.Count(x => x == '/') > 1)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -22,33 +20,33 @@ namespace DearFTP.Connection.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var newDirectory = session.NavigablePath.GetNewSystemFilePath(argument);
|
||||
var (navigablePath, realPath) = session.NavigablePath.GetNewSystemFilePath(argument);
|
||||
|
||||
if (newDirectory.navigablePath.CurrentDirectory == "/")
|
||||
if (navigablePath.CurrentDirectory == "/")
|
||||
{
|
||||
stream.Send(ResponseCode.FileUnavailable, "Can't modify the root.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newDirectory.realPath == null)
|
||||
if (realPath == null)
|
||||
{
|
||||
stream.Send(ResponseCode.FileUnavailable, "Invalid destination path.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(newDirectory.realPath) || Directory.Exists(newDirectory.realPath))
|
||||
if (File.Exists(realPath) || Directory.Exists(realPath))
|
||||
{
|
||||
stream.Send(ResponseCode.FileUnavailable, "Destination path already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.WritablesShares.Contains(newDirectory.navigablePath.CurrentShare))
|
||||
if (!session.WritablesShares.Contains(navigablePath.CurrentShare))
|
||||
{
|
||||
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(newDirectory.realPath);
|
||||
Directory.CreateDirectory(realPath);
|
||||
|
||||
stream.Send(ResponseCode.FileActionOK, "Directory successfully created.");
|
||||
}
|
||||
|
||||
21
DearFTP/Connection/Commands/OptionsCommand.cs
Normal file
21
DearFTP/Connection/Commands/OptionsCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class OptionsCommand : ICommand
|
||||
{
|
||||
public string[] Aliases { get; } = new string[]
|
||||
{
|
||||
"OPTS"
|
||||
};
|
||||
|
||||
public void Execute(Session session, FtpStream stream, string alias, string argument)
|
||||
{
|
||||
if (argument.ToUpper().Contains("UTF"))
|
||||
{
|
||||
stream.Send(ResponseCode.OK, "UTF8 supported on this server.");
|
||||
return;
|
||||
}
|
||||
|
||||
stream.Send(ResponseCode.ArgumentsError, "Requested option not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class ParentDirectoryCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DearFTP.Utils.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -20,7 +18,17 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
int port = session.DataConnection.Port;
|
||||
var portBytes = BitConverter.GetBytes((ushort)port).Reverse().Select(x => x.ToString());
|
||||
string remote = string.Join(',', session.IP.Split('.').Concat(portBytes));
|
||||
|
||||
string remote;
|
||||
|
||||
if (session.RemoteIP.IsInternal())
|
||||
{
|
||||
remote = string.Join(',', session.LocalIP.ToString().Split('.').Concat(portBytes));
|
||||
}
|
||||
else
|
||||
{
|
||||
remote = string.Join(',', session.Configuration.Server.Host.Split('.').Concat(portBytes));
|
||||
}
|
||||
|
||||
stream.Send(ResponseCode.PassiveMode, $"Entering Passive Mode ({remote})");
|
||||
|
||||
|
||||
21
DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs
Normal file
21
DearFTP/Connection/Commands/ProtectionBufferSizeCommand.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
36
DearFTP/Connection/Commands/ProtectionCommand.cs
Normal file
36
DearFTP/Connection/Commands/ProtectionCommand.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
{
|
||||
var dataConnection = session.DataConnection;
|
||||
|
||||
switch (argument.ToUpper())
|
||||
{
|
||||
case "C":
|
||||
if (dataConnection.IsTlsProtected)
|
||||
{
|
||||
session.DataConnection.DesactivateTls();
|
||||
}
|
||||
stream.Send(ResponseCode.OK, "Data protection cleared.");
|
||||
break;
|
||||
case "P":
|
||||
if (!dataConnection.IsTlsProtected)
|
||||
{
|
||||
session.DataConnection.ActivateTls();
|
||||
}
|
||||
stream.Send(ResponseCode.OK, "Data protection set.");
|
||||
break;
|
||||
default:
|
||||
stream.Send(ResponseCode.ArgumentsError, "Invalid argument.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class PwdCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class QuitCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
|
||||
23
DearFTP/Connection/Commands/RestartCommand.cs
Normal file
23
DearFTP/Connection/Commands/RestartCommand.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class RestartCommand : ICommand
|
||||
{
|
||||
public string[] Aliases { get; } = new string[]
|
||||
{
|
||||
"REST"
|
||||
};
|
||||
|
||||
public void Execute(Session session, FtpStream stream, string alias, string argument)
|
||||
{
|
||||
if (uint.TryParse(argument, out uint position))
|
||||
{
|
||||
session.RestartPosition = (int)position;
|
||||
stream.Send(ResponseCode.PendingFurtherInformation, $"Restart position set to {position}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.Send(ResponseCode.ArgumentsError, "Specified position is not a valid number.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -18,7 +16,7 @@ namespace DearFTP.Connection.Commands
|
||||
{
|
||||
var dataConnection = session.DataConnection;
|
||||
|
||||
if (!dataConnection.IsAvailable)
|
||||
if (!dataConnection.IsTlsProtected && !dataConnection.IsAvailable)
|
||||
{
|
||||
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
|
||||
return;
|
||||
@@ -40,28 +38,52 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
stream.Send(ResponseCode.FileStatusOK, "File coming.");
|
||||
|
||||
SendFile(dataConnection.Stream, realPath);
|
||||
dataConnection.Authenticate();
|
||||
|
||||
if (dataConnection.IsTlsProtected && !dataConnection.IsAvailable)
|
||||
{
|
||||
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
|
||||
return;
|
||||
}
|
||||
|
||||
int restartPosition = session.RestartPosition;
|
||||
session.RestartPosition = 0;
|
||||
|
||||
SendFile(dataConnection.Stream, realPath, restartPosition);
|
||||
|
||||
dataConnection.Close();
|
||||
|
||||
stream.Send(ResponseCode.CloseDataConnection, "File sent.");
|
||||
}
|
||||
|
||||
private void SendFile(Stream stream, string path)
|
||||
private void SendFile(Stream stream, string path, int offset)
|
||||
{
|
||||
using (var file = File.OpenRead(path))
|
||||
{
|
||||
if (offset != 0)
|
||||
{
|
||||
file.Seek(offset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
Span<byte> buffer = stackalloc byte[BufferSize];
|
||||
long bytesToWrite = Math.Min(file.Length - file.Position, BufferSize);
|
||||
|
||||
while (file.Read(buffer) > 0)
|
||||
{
|
||||
if (bytesToWrite < BufferSize)
|
||||
try
|
||||
{
|
||||
stream.Write(buffer.Slice(0, (int)bytesToWrite));
|
||||
break;
|
||||
if (bytesToWrite < BufferSize)
|
||||
{
|
||||
stream.Write(buffer.Slice(0, (int)bytesToWrite));
|
||||
break;
|
||||
}
|
||||
|
||||
stream.Write(buffer);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
stream.Write(buffer);
|
||||
|
||||
bytesToWrite = Math.Min(file.Length - file.Position, BufferSize);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class SiteCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using DearFTP.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -13,20 +10,24 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
public string[] Aliases { get; } = new string[]
|
||||
{
|
||||
"STOR"
|
||||
"APPE",
|
||||
"STOR",
|
||||
"STOU"
|
||||
};
|
||||
|
||||
public void Execute(Session session, FtpStream stream, string alias, string argument)
|
||||
{
|
||||
var dataConnection = session.DataConnection;
|
||||
|
||||
if (!dataConnection.IsAvailable)
|
||||
if (!dataConnection.IsTlsProtected && !dataConnection.IsAvailable)
|
||||
{
|
||||
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
|
||||
return;
|
||||
}
|
||||
|
||||
var (navigablePath, realPath) = session.NavigablePath.GetNewSystemFilePath(argument);
|
||||
string path = alias.ToUpper() == "STOU" ? Path.GetRandomFileName() : argument;
|
||||
|
||||
var (navigablePath, realPath) = session.NavigablePath.GetNewSystemFilePath(path);
|
||||
|
||||
if (realPath == null)
|
||||
{
|
||||
@@ -48,16 +49,24 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
stream.Send(ResponseCode.FileStatusOK, "Waiting file.");
|
||||
|
||||
ReceiveFile(dataConnection.Stream, realPath);
|
||||
dataConnection.Authenticate();
|
||||
|
||||
if (dataConnection.IsTlsProtected && !dataConnection.IsAvailable)
|
||||
{
|
||||
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
|
||||
return;
|
||||
}
|
||||
|
||||
ReceiveFile(dataConnection.Stream, realPath, alias.ToUpper() == "APPE");
|
||||
|
||||
dataConnection.Close();
|
||||
|
||||
stream.Send(ResponseCode.CloseDataConnection, "File received.");
|
||||
}
|
||||
|
||||
private void ReceiveFile(NetworkStream stream, string path)
|
||||
private void ReceiveFile(Stream stream, string path, bool append)
|
||||
{
|
||||
using (var file = File.Open(path, FileMode.Create))
|
||||
using (var file = File.Open(path, append ? FileMode.Append : FileMode.Create))
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[BufferSize];
|
||||
int readBytes;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class SystemCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
class TypeCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using DearFTP.Utils;
|
||||
using DearFTP.Utils.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection.Commands
|
||||
{
|
||||
@@ -17,10 +16,10 @@ namespace DearFTP.Connection.Commands
|
||||
{
|
||||
var configuration = FtpServer.Instance.Configuration;
|
||||
var user = configuration.Users.FirstOrDefault(x => x.Name == argument);
|
||||
string guest = configuration.Server.Guest;
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
string guest = configuration.Server.Guest;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(guest))
|
||||
{
|
||||
@@ -32,7 +31,7 @@ namespace DearFTP.Connection.Commands
|
||||
|
||||
user = configuration.Users.First(x => x.Name == guest);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(user.Password))
|
||||
if (!string.IsNullOrWhiteSpace(user.Password) && !(session.RemoteIP.IsInternal() && configuration.Server.BypassGuestPassword && user.Name == guest))
|
||||
{
|
||||
stream.Send(ResponseCode.NeedPassword, "Please specify the password.");
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
class DataConnection
|
||||
{
|
||||
public TcpListener Listener { get; private set; }
|
||||
public TcpClient Client { get; private set; }
|
||||
public NetworkStream Stream { get; private set; }
|
||||
public int Port => ((IPEndPoint)Listener.LocalEndpoint).Port;
|
||||
public bool IsAvailable { get; private set; }
|
||||
|
||||
public DataConnection()
|
||||
{
|
||||
IsAvailable = false;
|
||||
}
|
||||
|
||||
public void Create()
|
||||
{
|
||||
// Clean old connections
|
||||
if (Client?.Connected == true)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
Listener = new TcpListener(IPAddress.Any, 0);
|
||||
Listener.Start();
|
||||
|
||||
}
|
||||
|
||||
public void AcceptClient()
|
||||
{
|
||||
Client = Listener.AcceptTcpClient();
|
||||
Stream = Client.GetStream();
|
||||
|
||||
IsAvailable = true;
|
||||
}
|
||||
public async void AcceptClientAsync()
|
||||
{
|
||||
Client = await Listener.AcceptTcpClientAsync();
|
||||
Stream = Client.GetStream();
|
||||
|
||||
IsAvailable = true;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
IsAvailable = false;
|
||||
|
||||
Stream.Close();
|
||||
Client.Close();
|
||||
Listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
DearFTP/Connection/DynamicDataConnection.cs
Normal file
101
DearFTP/Connection/DynamicDataConnection.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
class DynamicDataConnection : IDataConnection
|
||||
{
|
||||
public const int Timeout = 10_000;
|
||||
|
||||
public Stream Stream { get; private set; }
|
||||
public bool IsTlsProtected { get; private set; }
|
||||
|
||||
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_acceptTask == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (_acceptTask.Wait(Timeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_acceptTask.Dispose();
|
||||
_acceptTask = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TcpListener _listener;
|
||||
private TcpClient _client;
|
||||
private Task _acceptTask;
|
||||
|
||||
public DynamicDataConnection()
|
||||
{
|
||||
IsTlsProtected = false;
|
||||
}
|
||||
|
||||
public void Create()
|
||||
{
|
||||
// Clean old connections
|
||||
if (_client?.Connected == true)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
_listener = new TcpListener(IPAddress.Any, 0);
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public void AcceptClient()
|
||||
{
|
||||
_acceptTask = _listener.AcceptTcpClientAsync().ContinueWith(t =>
|
||||
{
|
||||
_client = t.Result;
|
||||
|
||||
if (IsTlsProtected)
|
||||
{
|
||||
Stream = new SslStream(_client.GetStream(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Stream = _client.GetStream();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Authenticate()
|
||||
{
|
||||
if (IsTlsProtected)
|
||||
{
|
||||
((SslStream)Stream).AuthenticateAsServer(FtpServer.Instance.Configuration.Tls.X509Certificate, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateTls()
|
||||
{
|
||||
IsTlsProtected = true;
|
||||
}
|
||||
|
||||
public void DesactivateTls()
|
||||
{
|
||||
IsTlsProtected = false;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Stream.Close();
|
||||
_client.Close();
|
||||
_listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
@@ -10,18 +9,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 +48,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 +73,7 @@ namespace DearFTP.Connection
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(builder.ToString());
|
||||
|
||||
NetworkStream.Write(bytes, 0, bytes.Length);
|
||||
Stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
DearFTP/Connection/IDataConnection.cs
Normal file
19
DearFTP/Connection/IDataConnection.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.IO;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
interface IDataConnection
|
||||
{
|
||||
Stream Stream { get; }
|
||||
int Port { get; }
|
||||
bool IsTlsProtected { get; }
|
||||
bool IsAvailable { get; }
|
||||
|
||||
void Create();
|
||||
void AcceptClient();
|
||||
void Authenticate();
|
||||
void ActivateTls();
|
||||
void DesactivateTls();
|
||||
void Close();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
public enum ResponseCode : uint
|
||||
{
|
||||
@@ -175,6 +171,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,
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
using DearFTP.Connection.Commands;
|
||||
using DearFTP.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
@@ -19,14 +16,19 @@ namespace DearFTP.Connection
|
||||
public User User { get; set; }
|
||||
public Share[] Shares { get; set; }
|
||||
public Share[] WritablesShares { get; set; }
|
||||
public FtpStream FtpStream { get; }
|
||||
public NavigablePath NavigablePath { get; set; }
|
||||
public FtpStream FtpStream { get; private set; }
|
||||
public IDataConnection 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();
|
||||
public DataConnection DataConnection { get; set; }
|
||||
|
||||
private TcpClient _client;
|
||||
private NetworkStream _networkStream;
|
||||
public IPAddress LocalIP => ((IPEndPoint)_client.Client.LocalEndPoint).Address;
|
||||
public IPAddress RemoteIP => ((IPEndPoint)_client.Client.RemoteEndPoint).Address;
|
||||
|
||||
private readonly TcpClient _client;
|
||||
private readonly NetworkStream _networkStream;
|
||||
private SslStream _sslStream;
|
||||
private bool _isRunning = true;
|
||||
|
||||
public Session(TcpClient client)
|
||||
@@ -38,7 +40,9 @@ namespace DearFTP.Connection
|
||||
Configuration = FtpServer.Instance.Configuration;
|
||||
CommandsDispatcher = FtpServer.Instance.CommandsDispatcher;
|
||||
Logger = FtpServer.Instance.Logger;
|
||||
DataConnection = new DataConnection();
|
||||
DataConnection = Configuration.Server.ForceDataPort == 0 ? (IDataConnection)new DynamicDataConnection() : new StaticDataConnection();
|
||||
RestartPosition = 0;
|
||||
IsTlsProtected = false;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
@@ -48,7 +52,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);
|
||||
}
|
||||
@@ -61,31 +65,33 @@ 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();
|
||||
DataConnection.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
DearFTP/Connection/StaticDataConnection.cs
Normal file
112
DearFTP/Connection/StaticDataConnection.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DearFTP.Connection
|
||||
{
|
||||
class StaticDataConnection : IDataConnection
|
||||
{
|
||||
public const int Timeout = 10_000;
|
||||
|
||||
public Stream Stream { get; private set; }
|
||||
public bool IsTlsProtected { get; private set; }
|
||||
|
||||
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
public bool IsAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_acceptTask == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (_acceptTask.Wait(Timeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_acceptTask.Dispose();
|
||||
_acceptTask = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TcpListener _listener;
|
||||
private static Task _acceptTask;
|
||||
private TcpClient _client;
|
||||
|
||||
public StaticDataConnection()
|
||||
{
|
||||
if (_listener == null)
|
||||
{
|
||||
_listener = new TcpListener(IPAddress.Any, FtpServer.Instance.Configuration.Server.ForceDataPort);
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
IsTlsProtected = false;
|
||||
}
|
||||
|
||||
public void AcceptClient()
|
||||
{
|
||||
if (_acceptTask?.IsCompleted == false)
|
||||
{
|
||||
_acceptTask.Wait(Timeout);
|
||||
|
||||
_acceptTask.Dispose();
|
||||
_acceptTask = null;
|
||||
}
|
||||
|
||||
_acceptTask = _listener.AcceptTcpClientAsync().ContinueWith(t =>
|
||||
{
|
||||
_client = t.Result;
|
||||
|
||||
if (IsTlsProtected)
|
||||
{
|
||||
Stream = new SslStream(_client.GetStream(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Stream = _client.GetStream();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Authenticate()
|
||||
{
|
||||
if (IsTlsProtected)
|
||||
{
|
||||
((SslStream)Stream).AuthenticateAsServer(FtpServer.Instance.Configuration.Tls.X509Certificate, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateTls()
|
||||
{
|
||||
IsTlsProtected = true;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Stream.Close();
|
||||
_client.Close();
|
||||
}
|
||||
|
||||
public void Create()
|
||||
{
|
||||
// Nothing to create as the TcpListener is shared by all
|
||||
}
|
||||
|
||||
public void DesactivateTls()
|
||||
{
|
||||
IsTlsProtected = false;
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
_listener?.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DearFTP.Configurations;
|
||||
using DearFTP.Configurations;
|
||||
using DearFTP.Connection;
|
||||
using DearFTP.Connection.Commands;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DearFTP
|
||||
{
|
||||
@@ -21,6 +18,7 @@ namespace DearFTP
|
||||
|
||||
private bool _isRunning = true;
|
||||
private TcpListener _listener;
|
||||
private TcpListener _ftpsListener;
|
||||
|
||||
public FtpServer()
|
||||
{
|
||||
@@ -39,13 +37,21 @@ namespace DearFTP
|
||||
Logger.Log("DearFTP started");
|
||||
Logger.Log();
|
||||
|
||||
if (Configuration.Server.ImplicitFtpsPort != 0)
|
||||
{
|
||||
Task.Run(() => FtpsTcpLoop());
|
||||
}
|
||||
|
||||
TcpLoop();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
StaticDataConnection.Stop();
|
||||
_listener.Stop();
|
||||
_ftpsListener?.Stop();
|
||||
Configuration.Tls.X509Certificate?.Dispose();
|
||||
}
|
||||
|
||||
private void TcpLoop()
|
||||
@@ -63,12 +69,33 @@ namespace DearFTP
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSession(TcpClient client)
|
||||
private void FtpsTcpLoop()
|
||||
{
|
||||
_ftpsListener = new TcpListener(IPAddress.Any, Configuration.Server.ImplicitFtpsPort);
|
||||
_ftpsListener.Start(5);
|
||||
|
||||
Logger.Log($"Listening on: {_ftpsListener.LocalEndpoint}");
|
||||
|
||||
while (_isRunning)
|
||||
{
|
||||
var client = _ftpsListener.AcceptTcpClient();
|
||||
|
||||
Task.Run(() => CreateSession(client, true));
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSession(TcpClient client, bool implicitFtps = false)
|
||||
{
|
||||
Logger.Log($"Client connected: {client.Client.RemoteEndPoint}");
|
||||
|
||||
using (var session = new Session(client))
|
||||
{
|
||||
if (implicitFtps)
|
||||
{
|
||||
session.ActivateTls();
|
||||
session.DataConnection.ActivateTls();
|
||||
}
|
||||
|
||||
session.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DearFTP.Configurations;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using DearFTP.Configurations;
|
||||
|
||||
namespace DearFTP
|
||||
{
|
||||
@@ -10,12 +8,12 @@ namespace DearFTP
|
||||
{
|
||||
|
||||
public bool WriteToFile { get; private set; }
|
||||
public string FilePath { get; private set; }
|
||||
public string FilePath { get; private set; }
|
||||
|
||||
public Logger(Configuration configuration)
|
||||
{
|
||||
WriteToFile = configuration.Server.LogFileEnabled;
|
||||
FilePath = configuration.Server.LogFilePath;
|
||||
FilePath = configuration.Server.LogFilePath;
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DearFTP
|
||||
@@ -25,13 +23,7 @@ namespace DearFTP
|
||||
|
||||
var server = new FtpServer();
|
||||
|
||||
Task.Run(() => server.Start());
|
||||
|
||||
while (Console.ReadLine() != "stop") { }
|
||||
|
||||
server.Stop();
|
||||
|
||||
Environment.Exit(0);
|
||||
server.Start();
|
||||
}
|
||||
|
||||
private static void ProceedArguments(string[] args)
|
||||
|
||||
32
DearFTP/Utils/Extensions/IPAddressExtensions.cs
Normal file
32
DearFTP/Utils/Extensions/IPAddressExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Utils.Extensions
|
||||
{
|
||||
public static class IPAddressExtensions
|
||||
{
|
||||
// Source: https://stackoverflow.com/a/39120248
|
||||
/// <summary>
|
||||
/// An extension method to determine if an IP address is internal, as specified in RFC1918
|
||||
/// </summary>
|
||||
/// <param name="toTest">The IP address that will be tested</param>
|
||||
/// <returns>Returns true if the IP is internal, false if it is external</returns>
|
||||
public static bool IsInternal(this IPAddress toTest)
|
||||
{
|
||||
byte[] bytes = toTest.GetAddressBytes();
|
||||
switch (bytes[0])
|
||||
{
|
||||
case 10:
|
||||
return true;
|
||||
case 172:
|
||||
return bytes[1] < 32 && bytes[1] >= 16;
|
||||
case 192:
|
||||
return bytes[1] == 168;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Utils
|
||||
{
|
||||
@@ -28,7 +27,7 @@ namespace DearFTP.Utils
|
||||
CurrentShare = navigablePath.CurrentShare;
|
||||
CurrentDirectory = navigablePath.CurrentDirectory;
|
||||
|
||||
_path = new Stack<string>(navigablePath._path);
|
||||
_path = new Stack<string>(navigablePath._path.Reverse());
|
||||
}
|
||||
|
||||
public string GetFilePath(string fileName)
|
||||
@@ -138,7 +137,7 @@ namespace DearFTP.Utils
|
||||
return (tempNavigablePath, null);
|
||||
}
|
||||
|
||||
return (tempNavigablePath, Path.Combine(GetRealPath(), split.Last()));
|
||||
return (tempNavigablePath, Path.Combine(tempNavigablePath.GetRealPath(), split.Last()));
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetDirectories()
|
||||
@@ -193,7 +192,7 @@ namespace DearFTP.Utils
|
||||
|
||||
public bool NavigateTo(string path)
|
||||
{
|
||||
var oldPath = new Stack<string>(_path);
|
||||
var oldPath = new Stack<string>(_path.Reverse());
|
||||
var queue = new Queue<string>(path.Split('/'));
|
||||
|
||||
// If absolute path
|
||||
@@ -205,7 +204,7 @@ namespace DearFTP.Utils
|
||||
|
||||
if (NavigateTo(queue))
|
||||
{
|
||||
CurrentDirectory = $"/{string.Join('/', _path)}";
|
||||
CurrentDirectory = $"/{string.Join('/', _path.Reverse())}";
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -269,7 +268,7 @@ namespace DearFTP.Utils
|
||||
return null;
|
||||
}
|
||||
|
||||
return Path.Combine(_path.Skip(1).Prepend(CurrentShare.Path).ToArray());
|
||||
return Path.Combine(_path.Reverse().Skip(1).Prepend(CurrentShare.Path).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
530
DearFTP/Utils/OpenSslKey.cs
Normal file
530
DearFTP/Utils/OpenSslKey.cs
Normal file
@@ -0,0 +1,530 @@
|
||||
//**********************************************************************************
|
||||
//
|
||||
//OpenSSLKey
|
||||
// .NET 2.0 OpenSSL Public & Private Key Parser
|
||||
//
|
||||
// Copyright (C) 2008 JavaScience Consulting
|
||||
//
|
||||
//***********************************************************************************
|
||||
//
|
||||
// opensslkey.cs
|
||||
//
|
||||
// Reads and parses:
|
||||
// (1) OpenSSL PEM or DER public keys
|
||||
// (2) OpenSSL PEM or DER traditional SSLeay private keys (encrypted and unencrypted)
|
||||
// (3) PKCS #8 PEM or DER encoded private keys (encrypted and unencrypted)
|
||||
// Keys in PEM format must have headers/footers .
|
||||
// Encrypted Private Key in SSLEay format not supported in DER
|
||||
// Removes header/footer lines.
|
||||
// For traditional SSLEAY PEM private keys, checks for encrypted format and
|
||||
// uses PBE to extract 3DES key.
|
||||
// For SSLEAY format, only supports encryption format: DES-EDE3-CBC
|
||||
// For PKCS #8, only supports PKCS#5 v2.0 3des.
|
||||
// Parses private and public key components and returns .NET RSA object.
|
||||
// Creates dummy unsigned certificate linked to private keypair and
|
||||
// optionally exports to pkcs #12
|
||||
//
|
||||
// See also:
|
||||
// http://www.openssl.org/docs/crypto/pem.html#PEM_ENCRYPTION_FORMAT
|
||||
//**************************************************************************************
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace DearFTP.Utils
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct CRYPT_KEY_PROV_INFO
|
||||
{
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string pwszContainerName;
|
||||
[MarshalAs(UnmanagedType.LPWStr)] public string pwszProvName;
|
||||
public uint dwProvType;
|
||||
public uint dwFlags;
|
||||
public uint cProvParam;
|
||||
public IntPtr rgProvParam;
|
||||
public uint dwKeySpec;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct CERT_NAME_BLOB
|
||||
{
|
||||
public int cbData;
|
||||
public IntPtr pbData;
|
||||
}
|
||||
|
||||
public class OpenSslKey
|
||||
{
|
||||
//-------- Get the binary PKCS #8 PRIVATE key --------
|
||||
public static byte[] DecodePkcs8PrivateKey(string instr)
|
||||
{
|
||||
const string pemp8header = "-----BEGIN PRIVATE KEY-----";
|
||||
const string pemp8footer = "-----END PRIVATE KEY-----";
|
||||
string pemstr = instr.Trim();
|
||||
byte[] binkey;
|
||||
if (!pemstr.StartsWith(pemp8header) || !pemstr.EndsWith(pemp8footer))
|
||||
return null;
|
||||
StringBuilder sb = new StringBuilder(pemstr);
|
||||
sb.Replace(pemp8header, ""); //remove headers/footers, if present
|
||||
sb.Replace(pemp8footer, "");
|
||||
|
||||
string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace
|
||||
|
||||
try
|
||||
{
|
||||
binkey = Convert.FromBase64String(pubstr);
|
||||
}
|
||||
catch (FormatException)
|
||||
{ //if can't b64 decode, data is not valid
|
||||
return null;
|
||||
}
|
||||
return binkey;
|
||||
}
|
||||
|
||||
//------- Parses binary asn.1 PKCS #8 PrivateKeyInfo; returns RSACryptoServiceProvider ---
|
||||
public static RSACryptoServiceProvider DecodePrivateKeyInfo(byte[] pkcs8)
|
||||
{
|
||||
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
|
||||
// this byte[] includes the sequence byte and terminal encoded null
|
||||
byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
|
||||
byte[] seq;
|
||||
|
||||
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
|
||||
MemoryStream mem = new MemoryStream(pkcs8);
|
||||
int lenstream = (int)mem.Length;
|
||||
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
||||
byte bt;
|
||||
ushort twobytes;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
||||
binr.ReadByte(); //advance 1 byte
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16(); //advance 2 bytes
|
||||
else
|
||||
return null;
|
||||
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x02)
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
|
||||
if (twobytes != 0x0001)
|
||||
return null;
|
||||
|
||||
seq = binr.ReadBytes(15); //read the Sequence OID
|
||||
if (!CompareBytearrays(seq, SeqOID)) //make sure Sequence for OID is correct
|
||||
return null;
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x04) //expect an Octet string
|
||||
return null;
|
||||
|
||||
bt = binr.ReadByte(); //read next byte, or next 2 bytes is 0x81 or 0x82; otherwise bt is the byte count
|
||||
if (bt == 0x81)
|
||||
binr.ReadByte();
|
||||
else
|
||||
if (bt == 0x82)
|
||||
binr.ReadUInt16();
|
||||
//------ at this stage, the remaining sequence should be the RSA private key
|
||||
|
||||
byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
|
||||
RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey(rsaprivkey);
|
||||
return rsacsp;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
binr.Close();
|
||||
}
|
||||
}
|
||||
|
||||
//-------- Get the binary PKCS #8 Encrypted PRIVATE key --------
|
||||
public static byte[] DecodePkcs8EncPrivateKey(string instr)
|
||||
{
|
||||
const string pemp8encheader = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
||||
const string pemp8encfooter = "-----END ENCRYPTED PRIVATE KEY-----";
|
||||
string pemstr = instr.Trim();
|
||||
byte[] binkey;
|
||||
if (!pemstr.StartsWith(pemp8encheader) || !pemstr.EndsWith(pemp8encfooter))
|
||||
return null;
|
||||
StringBuilder sb = new StringBuilder(pemstr);
|
||||
sb.Replace(pemp8encheader, ""); //remove headers/footers, if present
|
||||
sb.Replace(pemp8encfooter, "");
|
||||
|
||||
string pubstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace
|
||||
|
||||
try
|
||||
{
|
||||
binkey = Convert.FromBase64String(pubstr);
|
||||
}
|
||||
catch (System.FormatException)
|
||||
{ //if can't b64 decode, data is not valid
|
||||
return null;
|
||||
}
|
||||
return binkey;
|
||||
}
|
||||
|
||||
//------- Parses binary asn.1 EncryptedPrivateKeyInfo; returns RSACryptoServiceProvider ---
|
||||
public static RSACryptoServiceProvider DecodeEncryptedPrivateKeyInfo(byte[] encpkcs8)
|
||||
{
|
||||
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
|
||||
// this byte[] includes the sequence byte and terminal encoded null
|
||||
byte[] OIDpkcs5PBES2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0D };
|
||||
byte[] OIDpkcs5PBKDF2 = { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0C };
|
||||
byte[] OIDdesEDE3CBC = { 0x06, 0x08, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x03, 0x07 };
|
||||
byte[] seqdes;
|
||||
byte[] seq;
|
||||
byte[] salt;
|
||||
byte[] IV;
|
||||
byte[] encryptedpkcs8;
|
||||
byte[] pkcs8;
|
||||
|
||||
int saltsize, ivsize, encblobsize;
|
||||
int iterations;
|
||||
|
||||
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
|
||||
MemoryStream mem = new MemoryStream(encpkcs8);
|
||||
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
||||
byte bt;
|
||||
ushort twobytes;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
||||
binr.ReadByte(); //advance 1 byte
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16(); //advance 2 bytes
|
||||
else
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16(); //inner sequence
|
||||
if (twobytes == 0x8130)
|
||||
binr.ReadByte();
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16();
|
||||
|
||||
|
||||
seq = binr.ReadBytes(11); //read the Sequence OID
|
||||
if (!CompareBytearrays(seq, OIDpkcs5PBES2)) //is it a OIDpkcs5PBES2 ?
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16(); //inner sequence for pswd salt
|
||||
if (twobytes == 0x8130)
|
||||
binr.ReadByte();
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16();
|
||||
|
||||
twobytes = binr.ReadUInt16(); //inner sequence for pswd salt
|
||||
if (twobytes == 0x8130)
|
||||
binr.ReadByte();
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16();
|
||||
|
||||
seq = binr.ReadBytes(11); //read the Sequence OID
|
||||
if (!CompareBytearrays(seq, OIDpkcs5PBKDF2)) //is it a OIDpkcs5PBKDF2 ?
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes == 0x8130)
|
||||
binr.ReadByte();
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16();
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x04) //expect octet string for salt
|
||||
return null;
|
||||
saltsize = binr.ReadByte();
|
||||
salt = binr.ReadBytes(saltsize);
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x02) //expect an integer for PBKF2 interation count
|
||||
return null;
|
||||
|
||||
int itbytes = binr.ReadByte(); //PBKD2 iterations should fit in 2 bytes.
|
||||
if (itbytes == 1)
|
||||
iterations = binr.ReadByte();
|
||||
else if (itbytes == 2)
|
||||
iterations = 256 * binr.ReadByte() + binr.ReadByte();
|
||||
else
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes == 0x8130)
|
||||
binr.ReadByte();
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16();
|
||||
|
||||
seqdes = binr.ReadBytes(10); //read the Sequence OID
|
||||
if (!CompareBytearrays(seqdes, OIDdesEDE3CBC)) //is it a OIDdes-EDE3-CBC ?
|
||||
return null;
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x04) //expect octet string for IV
|
||||
return null;
|
||||
|
||||
ivsize = binr.ReadByte(); // IV byte size should fit in one byte (24 expected for 3DES)
|
||||
IV = binr.ReadBytes(ivsize);
|
||||
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x04) // expect octet string for encrypted PKCS8 data
|
||||
return null;
|
||||
|
||||
bt = binr.ReadByte();
|
||||
|
||||
if (bt == 0x81)
|
||||
encblobsize = binr.ReadByte(); // data size in next byte
|
||||
else if (bt == 0x82)
|
||||
encblobsize = 256 * binr.ReadByte() + binr.ReadByte();
|
||||
else
|
||||
encblobsize = bt; // we already have the data size
|
||||
|
||||
encryptedpkcs8 = binr.ReadBytes(encblobsize);
|
||||
|
||||
SecureString secpswd = GetSecPswd("Enter password for Encrypted PKCS #8 ==>");
|
||||
pkcs8 = DecryptPBDK2(encryptedpkcs8, salt, IV, secpswd, iterations);
|
||||
|
||||
if (pkcs8 == null) // probably a bad pswd entered.
|
||||
return null;
|
||||
|
||||
//----- With a decrypted pkcs #8 PrivateKeyInfo blob, decode it to an RSA ---
|
||||
RSACryptoServiceProvider rsa = DecodePrivateKeyInfo(pkcs8);
|
||||
|
||||
return rsa;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
binr.Close();
|
||||
}
|
||||
}
|
||||
|
||||
// ------ Uses PBKD2 to derive a 3DES key and decrypts data --------
|
||||
public static byte[] DecryptPBDK2(byte[] edata, byte[] salt, byte[] IV, SecureString secpswd, int iterations)
|
||||
{
|
||||
byte[] psbytes = new byte[secpswd.Length];
|
||||
IntPtr unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd);
|
||||
Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length);
|
||||
Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd);
|
||||
|
||||
try
|
||||
{
|
||||
Rfc2898DeriveBytes kd = new Rfc2898DeriveBytes(psbytes, salt, iterations);
|
||||
TripleDES decAlg = TripleDES.Create();
|
||||
decAlg.Key = kd.GetBytes(24);
|
||||
decAlg.IV = IV;
|
||||
MemoryStream memstr = new MemoryStream();
|
||||
CryptoStream decrypt = new CryptoStream(memstr, decAlg.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
decrypt.Write(edata, 0, edata.Length);
|
||||
decrypt.Flush();
|
||||
decrypt.Close(); // this is REQUIRED.
|
||||
byte[] cleartext = memstr.ToArray();
|
||||
return cleartext;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Problem decrypting: {0}", e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider ---
|
||||
public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey)
|
||||
{
|
||||
byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
|
||||
|
||||
// --------- Set up stream to decode the asn.1 encoded RSA private key ------
|
||||
MemoryStream mem = new MemoryStream(privkey);
|
||||
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
|
||||
byte bt;
|
||||
ushort twobytes;
|
||||
int elems;
|
||||
|
||||
try
|
||||
{
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
|
||||
binr.ReadByte(); //advance 1 byte
|
||||
else if (twobytes == 0x8230)
|
||||
binr.ReadInt16(); //advance 2 bytes
|
||||
else
|
||||
return null;
|
||||
|
||||
twobytes = binr.ReadUInt16();
|
||||
if (twobytes != 0x0102) //version number
|
||||
return null;
|
||||
bt = binr.ReadByte();
|
||||
if (bt != 0x00)
|
||||
return null;
|
||||
|
||||
|
||||
//------ all private key components are Integer sequences ----
|
||||
elems = GetIntegerSize(binr);
|
||||
MODULUS = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
E = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
D = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
P = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
Q = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
DP = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
DQ = binr.ReadBytes(elems);
|
||||
|
||||
elems = GetIntegerSize(binr);
|
||||
IQ = binr.ReadBytes(elems);
|
||||
|
||||
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
|
||||
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
|
||||
RSAParameters RSAparams = new RSAParameters
|
||||
{
|
||||
Modulus = MODULUS,
|
||||
Exponent = E,
|
||||
D = D,
|
||||
P = P,
|
||||
Q = Q,
|
||||
DP = DP,
|
||||
DQ = DQ,
|
||||
InverseQ = IQ
|
||||
};
|
||||
|
||||
RSA.ImportParameters(RSAparams);
|
||||
|
||||
return RSA;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally { binr.Close(); }
|
||||
}
|
||||
|
||||
private static int GetIntegerSize(BinaryReader binr)
|
||||
{
|
||||
byte bt = binr.ReadByte();
|
||||
|
||||
if (bt != 0x02) //expect integer
|
||||
|
||||
return 0;
|
||||
bt = binr.ReadByte();
|
||||
|
||||
int count;
|
||||
if (bt == 0x81)
|
||||
count = binr.ReadByte(); // data size in next byte
|
||||
|
||||
else if (bt == 0x82)
|
||||
{
|
||||
byte highbyte = binr.ReadByte();
|
||||
byte lowbyte = binr.ReadByte();
|
||||
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
|
||||
count = BitConverter.ToInt32(modint, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
count = bt; // we already have the data size
|
||||
}
|
||||
|
||||
while (binr.ReadByte() == 0x00)
|
||||
{ //remove high order zeros in data
|
||||
count -= 1;
|
||||
}
|
||||
|
||||
binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
|
||||
return count;
|
||||
}
|
||||
|
||||
private static SecureString GetSecPswd(string prompt)
|
||||
{
|
||||
SecureString password = new SecureString();
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.Write(prompt);
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
|
||||
while (true)
|
||||
{
|
||||
ConsoleKeyInfo cki = Console.ReadKey(true);
|
||||
if (cki.Key == ConsoleKey.Enter)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine();
|
||||
return password;
|
||||
}
|
||||
else if (cki.Key == ConsoleKey.Backspace)
|
||||
{
|
||||
// remove the last asterisk from the screen...
|
||||
if (password.Length > 0)
|
||||
{
|
||||
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
|
||||
Console.Write(" ");
|
||||
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
|
||||
password.RemoveAt(password.Length - 1);
|
||||
}
|
||||
}
|
||||
else if (cki.Key == ConsoleKey.Escape)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.WriteLine();
|
||||
return password;
|
||||
}
|
||||
else if (char.IsLetterOrDigit(cki.KeyChar) || char.IsSymbol(cki.KeyChar))
|
||||
{
|
||||
if (password.Length < 20)
|
||||
{
|
||||
password.AppendChar(cki.KeyChar);
|
||||
Console.Write("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Beep();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CompareBytearrays(byte[] a, byte[] b)
|
||||
{
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (byte c in a)
|
||||
{
|
||||
if (c != b[i])
|
||||
return false;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user