diff --git a/MTSC-TestServer/Program.cs b/MTSC-TestServer/Program.cs index 57376af..359a2c5 100644 --- a/MTSC-TestServer/Program.cs +++ b/MTSC-TestServer/Program.cs @@ -1,18 +1,14 @@ using MTSC.Common.Ftp.FtpModules; -using MTSC.Common.Http.ServerModules; -using MTSC.Common.WebSockets.ServerModules; using MTSC.Exceptions; using MTSC.Logging; using MTSC.ServerSide; using MTSC.ServerSide.Handlers; using MTSC.ServerSide.UsageMonitors; -using System; -using System.Numerics; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace MTSC_TestServer -{ +{ class Program { static void Main(string[] args) @@ -33,10 +29,13 @@ namespace MTSC_TestServer .AddHandler(new FtpHandler() .AddModule(new AuthenticationModule()) .AddModule(new SystModule()) - .AddModule(new FileSystemModule() - .WithRootPath("src/")) + .AddModule(new DataConnectionModule()) + .AddModule(new DirectoryModule() + .WithRootPath("src")) + .AddModule(new FileModule()) .AddModule(new QuitModule()) .AddModule(new UnknownCommandModule())) + .WithClientCertificate(true) .Run(); } diff --git a/MTSC.UnitTests/E2ETests.cs b/MTSC.UnitTests/E2ETests.cs index 374c34b..af8a579 100644 --- a/MTSC.UnitTests/E2ETests.cs +++ b/MTSC.UnitTests/E2ETests.cs @@ -1,4 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using MTSC.Common.Ftp; +using MTSC.Common.Ftp.FtpModules; using MTSC.Common.Http; using MTSC.Common.Http.RoutingModules; using MTSC.Common.Http.ServerModules; @@ -33,7 +35,6 @@ namespace MTSC.UnitTests public static void InitializeServer(TestContext testContext) { Server = new ServerSide.Server(800) - .WithCertificate(new X509Certificate2("powershellcert.pfx", "123")) .AddHandler(new WebsocketRoutingHandler() .AddRoute("echo", new EchoWebsocketModule() .WithReceiveTemplateProvider((message) => UTF8Encoding.UTF8.GetString(message.Data)) @@ -51,8 +52,6 @@ namespace MTSC.UnitTests .AddRoute(HttpMessage.HttpMethods.Get, "long-running", new LongRunningModule()) .WithFragmentsExpirationTime(TimeSpan.FromMilliseconds(500)) .WithMaximumSize(300)) - .AddHandler(new FtpHandler() - .WithReadyDelay(TimeSpan.FromMilliseconds(500))) .AddLogger(new ConsoleLogger()) .AddLogger(new DebugConsoleLogger()) .AddExceptionHandler(new ExceptionConsoleLogger()) @@ -247,20 +246,6 @@ namespace MTSC.UnitTests TestContext.WriteLine($"{i}: Processed {tasks.Length} requests in {duration.TotalMilliseconds} ms."); } } - - [TestMethod] - public void FtpTest() - { - FtpWebRequest request = (FtpWebRequest)WebRequest.Create("ftp://127.0.0.1:800"); - request.Method = WebRequestMethods.Ftp.ListDirectory; - request.UsePassive = true; - request.EnableSsl = true; - request.Credentials = new NetworkCredential("user", "password"); - using (FtpWebResponse response = (FtpWebResponse)request.GetResponse()) - { - - } - } [ClassCleanup] public static void CleanupServer() diff --git a/MTSC/Common/Ftp/FtpData.cs b/MTSC/Common/Ftp/FtpData.cs index cf5edf3..432df44 100644 --- a/MTSC/Common/Ftp/FtpData.cs +++ b/MTSC/Common/Ftp/FtpData.cs @@ -28,6 +28,7 @@ namespace MTSC.Common.Ftp { this.TransferDetails.Socket.Close(); this.TransferDetails.Socket.Dispose(); + this.TransferDetails.ConnectionOpen = false; } public void SendData(byte[] data, int length) diff --git a/MTSC/Common/Ftp/FtpModules/AuthenticationModule.cs b/MTSC/Common/Ftp/FtpModules/AuthenticationModule.cs index c6aa97c..704f118 100644 --- a/MTSC/Common/Ftp/FtpModules/AuthenticationModule.cs +++ b/MTSC/Common/Ftp/FtpModules/AuthenticationModule.cs @@ -7,6 +7,12 @@ namespace MTSC.Common.Ftp.FtpModules public class AuthenticationModule : IFtpModule { public Func ValidateAuthentication { get; set; } = AlwaysAllowed; + + public AuthenticationModule WithAuthenticationValidation(Func func) + { + this.ValidateAuthentication = func; + return this; + } bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server) { if (!client.Resources.TryGetResource(out var authData)) diff --git a/MTSC/Common/Ftp/FtpModules/DataConnectionModule.cs b/MTSC/Common/Ftp/FtpModules/DataConnectionModule.cs new file mode 100644 index 0000000..9e6300d --- /dev/null +++ b/MTSC/Common/Ftp/FtpModules/DataConnectionModule.cs @@ -0,0 +1,105 @@ +using MTSC.ServerSide; +using MTSC.ServerSide.Handlers; +using System; +using System.Net; +using System.Net.Sockets; + +namespace MTSC.Common.Ftp.FtpModules +{ + public class DataConnectionModule : IFtpModule + { + bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server) + { + if (!client.Resources.TryGetResource(out var ftpData)) + { + client.Resources.SetResource(new FtpData()); + } + + if (request.Command == FtpRequestCommands.PASV) + { + handler.QueueFtpResponse(server, client, EnablePassiveMode(request, ftpData, client)); + return true; + } + else if (request.Command == FtpRequestCommands.PORT) + { + handler.QueueFtpResponse(server, client, EnableActiveMode(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.TYPE) + { + handler.QueueFtpResponse(server, client, HandleTypeArguments(request, ftpData)); + return true; + } + + return false; + } + + private FtpResponse EnableActiveMode(FtpRequest request, FtpData ftpData) + { + ftpData.TransferDetails.Socket?.Dispose(); + + var portArgs = request.Arguments[0].Split(','); + + ftpData.TransferDetails.Mode = TransferDetails.TransferMode.Active; + byte[] addressBytes = { byte.Parse(portArgs[0]), byte.Parse(portArgs[1]), byte.Parse(portArgs[2]), byte.Parse(portArgs[3]) }; + byte[] portBytes = { byte.Parse(portArgs[4]), byte.Parse(portArgs[5]) }; + + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(portBytes); + } + + var address = new IPAddress(addressBytes); + var port = BitConverter.ToUInt16(portBytes, 0); + + ftpData.TransferDetails.DestinationDataAddress = address; + ftpData.TransferDetails.DestinationDataPort = port; + + ftpData.TransferDetails.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "PORT Command Accepted" }; + } + + private FtpResponse EnablePassiveMode(FtpRequest request, FtpData ftpData, ClientData client) + { + ftpData.TransferDetails.Socket?.Dispose(); + + ftpData.TransferDetails.Mode = TransferDetails.TransferMode.Passive; + ftpData.TransferDetails.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + ftpData.TransferDetails.Socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + byte[] address = ((IPEndPoint)client.TcpClient.Client.LocalEndPoint).Address.GetAddressBytes(); + byte[] portBytes = BitConverter.GetBytes((short)ftpData.TransferDetails.LocalDataPort); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(portBytes); + } + + ftpData.TransferDetails.Socket.Listen(10); + + return new FtpResponse + { + StatusCode = FtpResponseCodes.EnterPassiveMode, + Message = $"Entering Passive mode ({address[0]},{address[1]},{address[2]},{address[3]},{portBytes[0]},{portBytes[1]})" + }; + } + + private FtpResponse HandleTypeArguments(FtpRequest request, FtpData ftpData) + { + if (request.Arguments[0] == "I") + { + ftpData.TransferDetails.Type = TransferDetails.TransferType.BINARY; + return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "Set transfer encoding to binary" }; + } + else if (request.Arguments[0] == "A") + { + ftpData.TransferDetails.Type = TransferDetails.TransferType.ASCII; + return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "Set transfer encoding to ASCII" }; + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.CommandNotImplementedForParameter, Message = $"Command not implemented for parameter {request.Arguments[0]}" }; + } + } + } +} diff --git a/MTSC/Common/Ftp/FtpModules/DirectoryModule.cs b/MTSC/Common/Ftp/FtpModules/DirectoryModule.cs new file mode 100644 index 0000000..1c6935c --- /dev/null +++ b/MTSC/Common/Ftp/FtpModules/DirectoryModule.cs @@ -0,0 +1,214 @@ +using MTSC.ServerSide; +using MTSC.ServerSide.Handlers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace MTSC.Common.Ftp.FtpModules +{ + public class DirectoryModule : IFtpModule + { + public string RootPath { get; set; } = Path.GetFullPath("src/"); + + public DirectoryModule WithRootPath(string rootPath) + { + this.RootPath = rootPath; + return this; + } + + bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server) + { + if (!Directory.Exists(Path.GetFullPath(RootPath))) + { + server.Log($"Root path [{Path.GetFullPath(RootPath)}] doesn't exist! Creating it now!"); + Directory.CreateDirectory(Path.GetFullPath(RootPath)); + } + + if (!client.Resources.TryGetResource(out var ftpData)) + { + client.Resources.SetResource(new FtpData { CurrentDirectory = Path.GetFullPath(RootPath) }); + } + + if (string.IsNullOrEmpty(ftpData.CurrentDirectory)) + { + ftpData.CurrentDirectory = Path.GetFullPath(this.RootPath); + } + + if (request.Command == FtpRequestCommands.MKD) + { + handler.QueueFtpResponse(server, client, CreateDirectory(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.PWD) + { + handler.QueueFtpResponse(server, client, PrintCurrentDirectory(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.CWD) + { + handler.QueueFtpResponse(server, client, ChangeCurrentDirectory(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.CDUP) + { + handler.QueueFtpResponse(server, client, ChangeToParentDirectory(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.RMD) + { + handler.QueueFtpResponse(server, client, RemoveDirectory(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.LIST) + { + if (!ftpData.TransferDetails.ConnectionOpen) + { + handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); + ftpData.OpenDataConnection(); + } + handler.QueueFtpResponse(server, client, ListFolder(request, ftpData)); + ftpData.CloseDataConnection(); + return true; + } + + return false; + } + + private FtpResponse PrintCurrentDirectory(FtpRequest request, FtpData ftpData) + { + var relativePath = ftpData.CurrentDirectory.RelativePath(Path.GetFullPath(this.RootPath)); + if (string.IsNullOrWhiteSpace(relativePath)) + { + relativePath = "\\"; + } + else + { + relativePath = relativePath.Substring(this.RootPath.Length) + "\\"; + } + return new FtpResponse { StatusCode = FtpResponseCodes.PATHNAMECreated, Message = $"\"{relativePath}\" is current directory" }; + } + + private FtpResponse ListFolder(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + ftpData.CurrentDirectory; + IEnumerable directories = Directory.EnumerateDirectories(path); + IEnumerable files = Directory.EnumerateFiles(path); + + foreach (string dir in directories) + { + DateTime editDate = Directory.GetLastWriteTime(dir); + + string date = editDate < DateTime.Now.Subtract(TimeSpan.FromDays(180)) ? + editDate.ToString("MMM dd yyyy", CultureInfo.InvariantCulture) : + editDate.ToString("MMM dd HH:mm", CultureInfo.InvariantCulture); + + var data = "drwxr-xr-x 2 2003 2003 4096 " + date + ' ' + Path.GetFileName(dir) + "\r\n"; + var bytes = Encoding.UTF8.GetBytes(data); + ftpData.SendData(bytes, bytes.Length); + } + + foreach (string file in files) + { + FileInfo f = new FileInfo(file); + + string date = f.LastWriteTime < DateTime.Now.Subtract(TimeSpan.FromDays(180)) ? + f.LastWriteTime.ToString("MMM dd yyyy", CultureInfo.InvariantCulture) : + f.LastWriteTime.ToString("MMM dd HH:mm", CultureInfo.InvariantCulture); + + var line = "-rw-r--r-- 2 2003 2003 "; + + var length = f.Length.ToString(CultureInfo.InvariantCulture); + + if (length.Length < 8) + { + for (int i = 0; i < 8 - length.Length; i++) + { + line += ' '; + } + } + + line += length + ' ' + date + ' ' + f.Name + "\r\n"; + var bytes = Encoding.UTF8.GetBytes(line); + ftpData.SendData(bytes, bytes.Length); + } + return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; + } + + private FtpResponse CreateDirectory(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + "\\" + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + null; + + if (path != null) + { + if (!path.IsSubPathOf(Path.GetFullPath(this.RootPath))) + { + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionNotTaken, Message = "Path not found!" }; + } + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = $"Created directory {request.Arguments[0]}!" }; + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionNotTaken, Message = $"Directory {request.Arguments[0]} already exists!" }; + } + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; + } + } + + private FtpResponse RemoveDirectory(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + "\\" + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + ftpData.CurrentDirectory; + + if (!path.IsSubPathOf(Path.GetFullPath(this.RootPath))) + { + return new FtpResponse { StatusCode = FtpResponseCodes.FileUnavailable, Message = "Directory not found!" }; + } + + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = "Removed directory!" }; + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.FileUnavailable, Message = "Directory not found!" }; + } + } + + private FtpResponse ChangeToParentDirectory(FtpRequest request, FtpData ftpData) + { + var directoryInfo = Directory.GetParent(ftpData.CurrentDirectory); + if (!directoryInfo.FullName.IsSubPathOf(Path.GetFullPath(RootPath))) + { + return new FtpResponse { StatusCode = FtpResponseCodes.SyntaxError, Message = "Cannot CDUP from root directory!" }; + } + else + { + ftpData.CurrentDirectory = directoryInfo.FullName; + return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = $"Changed working directory!" }; + } + } + + private FtpResponse ChangeCurrentDirectory(FtpRequest request, FtpData ftpData) + { + var directory = this.RootPath + request.Arguments.Aggregate((prev, next) => prev + " " + next); + ftpData.CurrentDirectory = Path.GetFullPath(directory); + return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "Changed working directory!" }; + } + } +} diff --git a/MTSC/Common/Ftp/FtpModules/FileModule.cs b/MTSC/Common/Ftp/FtpModules/FileModule.cs new file mode 100644 index 0000000..7abd808 --- /dev/null +++ b/MTSC/Common/Ftp/FtpModules/FileModule.cs @@ -0,0 +1,118 @@ +using MTSC.ServerSide; +using MTSC.ServerSide.Handlers; +using System; +using System.IO; +using System.Linq; + +namespace MTSC.Common.Ftp.FtpModules +{ + public class FileModule : IFtpModule + { + bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server) + { + if (!client.Resources.TryGetResource(out var ftpData)) + { + client.Resources.SetResource(new FtpData()); + } + + if (request.Command == FtpRequestCommands.STOR) + { + if (!ftpData.TransferDetails.ConnectionOpen) + { + handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); + ftpData.OpenDataConnection(); + } + handler.QueueFtpResponse(server, client, StoreFile(request, ftpData)); + ftpData.CloseDataConnection(); + return true; + } + else if (request.Command == FtpRequestCommands.DELE) + { + handler.QueueFtpResponse(server, client, RemoveFile(request, ftpData)); + return true; + } + else if (request.Command == FtpRequestCommands.RETR) + { + if (!ftpData.TransferDetails.ConnectionOpen) + { + handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); + ftpData.OpenDataConnection(); + } + handler.QueueFtpResponse(server, client, RetrieveFile(request, ftpData)); + ftpData.CloseDataConnection(); + return true; + } + + return false; + } + + private FtpResponse StoreFile(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + "\\" + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + null; + + if (path != null) + { + using (var fs = File.Create(path)) + { + while (ftpData.AvailableBytes() > 0) + { + var bytes = ftpData.GetBytes(ftpData.AvailableBytes()); + fs.Write(bytes, 0, bytes.Length); + } + } + return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; + } + } + + private FtpResponse RetrieveFile(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + null; + + if (path != null) + { + using (var fs = File.OpenRead(path)) + { + var buffer = new byte[256]; + int bytesRead; + while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) + { + ftpData.SendData(buffer, bytesRead); + } + } + } + return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; + } + + private FtpResponse RemoveFile(FtpRequest request, FtpData ftpData) + { + var path = request.Arguments.Length > 0 ? + Path.GetFullPath(ftpData.CurrentDirectory + "\\" + request.Arguments.Aggregate((prev, next) => prev + " " + next)) : + null; + + if (path != null) + { + if (File.Exists(path)) + { + File.Delete(path); + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = $"Removed file {request.Arguments[0]}!" }; + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.FileUnavailable, Message = $"File {request.Arguments[0]} not found!" }; + } + } + else + { + return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; + } + } + } +} diff --git a/MTSC/Common/Ftp/FtpModules/FileSystemModule.cs b/MTSC/Common/Ftp/FtpModules/FileSystemModule.cs deleted file mode 100644 index 28b8645..0000000 --- a/MTSC/Common/Ftp/FtpModules/FileSystemModule.cs +++ /dev/null @@ -1,396 +0,0 @@ -using MTSC.ServerSide; -using MTSC.ServerSide.Handlers; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; - -namespace MTSC.Common.Ftp.FtpModules -{ - public partial class FileSystemModule : IFtpModule - { - public string RootPath { get; set; } = Path.GetFullPath("/src/"); - - public FileSystemModule WithRootPath(string rootPath) - { - this.RootPath = Path.GetFullPath(rootPath); - return this; - } - - bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server) - { - if (!Directory.Exists(Path.GetFullPath(RootPath))) - { - server.Log($"Root path [{Path.GetFullPath(RootPath)}] doesn't exist! Creating it now!"); - Directory.CreateDirectory(Path.GetFullPath(RootPath)); - } - - if (!client.Resources.TryGetResource(out var ftpData)) - { - client.Resources.SetResource(new FtpData { CurrentDirectory = Path.GetFullPath(RootPath) }); - } - - if (request.Command == FtpRequestCommands.PWD) - { - handler.QueueFtpResponse(server, client, PrintCurrentDirectory(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.CWD) - { - handler.QueueFtpResponse(server, client, ChangeCurrentDirectory(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.CDUP) - { - handler.QueueFtpResponse(server, client, ChangeToParentDirectory(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.RMD) - { - handler.QueueFtpResponse(server, client, RemoveDirectory(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.DELE) - { - handler.QueueFtpResponse(server, client, RemoveFile(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.MKD) - { - handler.QueueFtpResponse(server, client, CreateDirectory(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.RETR) - { - if (!ftpData.TransferDetails.ConnectionOpen) - { - handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); - ftpData.OpenDataConnection(); - } - handler.QueueFtpResponse(server, client, RetrieveFile(request, ftpData)); - ftpData.CloseDataConnection(); - return true; - } - else if (request.Command == FtpRequestCommands.STOR) - { - if (!ftpData.TransferDetails.ConnectionOpen) - { - handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); - ftpData.OpenDataConnection(); - } - handler.QueueFtpResponse(server, client, StoreFile(request, ftpData)); - ftpData.CloseDataConnection(); - return true; - } - else if (request.Command == FtpRequestCommands.TYPE) - { - handler.QueueFtpResponse(server, client, HandleTypeArguments(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.PASV) - { - handler.QueueFtpResponse(server, client, EnablePassiveMode(request, ftpData, client)); - return true; - } - else if (request.Command == FtpRequestCommands.PORT) - { - handler.QueueFtpResponse(server, client, EnableActiveMode(request, ftpData)); - return true; - } - else if (request.Command == FtpRequestCommands.LIST) - { - if (!ftpData.TransferDetails.ConnectionOpen) - { - handler.QueueFtpResponse(server, client, new FtpResponse { StatusCode = FtpResponseCodes.FileStatusOkay, Message = "Opening data connection" }); - ftpData.OpenDataConnection(); - } - handler.QueueFtpResponse(server, client, ListFolder(request, ftpData)); - ftpData.CloseDataConnection(); - return true; - } - - return false; - } - - private FtpResponse StoreFile(FtpRequest request, FtpData ftpData) - { - var path = request.Arguments.Length > 0 ? Path.GetFullPath(request.Arguments[0]) : null; - - if (path != null) - { - using (var fs = File.Create(path)) - { - while(ftpData.AvailableBytes() > 0) - { - var bytes = ftpData.GetBytes(ftpData.AvailableBytes()); - fs.Write(bytes, 0, bytes.Length); - } - } - return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; - } - } - - private FtpResponse RetrieveFile(FtpRequest request, FtpData ftpData) - { - var path = request.Arguments.Length > 0 ? Path.GetFullPath(request.Arguments[0]) : null; - - if (path != null) - { - using (var fs = File.OpenRead(path)) - { - var buffer = new byte[256]; - int bytesRead; - while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) - { - ftpData.SendData(buffer, bytesRead); - } - } - } - return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; - } - - private FtpResponse CreateDirectory(FtpRequest request, FtpData ftpData) - { - var path = request.Arguments.Length > 0 ? Path.GetFullPath(request.Arguments[0]) : null; - - if (path != null) - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = $"Created directory {request.Arguments[0]}!" }; - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionNotTaken, Message = $"Directory {request.Arguments[0]} already exists!" }; - } - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; - } - } - - private FtpResponse RemoveFile(FtpRequest request, FtpData ftpData) - { - var path = request.Arguments.Length > 0 ? Path.GetFullPath(request.Arguments[0]) : null; - - if (path != null) - { - if (File.Exists(path)) - { - File.Delete(path); - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = $"Removed file {request.Arguments[0]}!" }; - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.FileUnavailable, Message = $"File {request.Arguments[0]} not found!" }; - } - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; - } - } - - private FtpResponse RemoveDirectory(FtpRequest request, FtpData ftpData) - { - var path = request.Arguments.Length > 0 ? Path.GetFullPath(request.Arguments[0]) : null; - - if(path != null) - { - if (Directory.Exists(path)) - { - Directory.Delete(path); - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedFileActionOkay, Message = $"Removed directory {request.Arguments[0]}!" }; - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.FileUnavailable, Message = $"Directory {request.Arguments[0]} not found!" }; - } - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.RequestedActionAborted, Message = $"No path argument provided!" }; - } - } - - private FtpResponse ChangeToParentDirectory(FtpRequest request, FtpData ftpData) - { - var directoryInfo = Directory.GetParent(ftpData.CurrentDirectory); - if (!directoryInfo.FullName.IsSubPathOf(RootPath)) - { - return new FtpResponse { StatusCode = FtpResponseCodes.SyntaxError, Message = "Cannot CDUP from root directory" }; - } - else - { - ftpData.CurrentDirectory = directoryInfo.FullName; - return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = $"Changed working directory to {directoryInfo.Name}" }; - } - } - - private FtpResponse ChangeCurrentDirectory(FtpRequest request, FtpData ftpData) - { - var directory = this.RootPath + request.Arguments[0]; - ftpData.CurrentDirectory = Path.GetFullPath(directory); - return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = $"Changed working directory to {request.Arguments[0]}" }; - } - - private FtpResponse EnableActiveMode(FtpRequest request, FtpData ftpData) - { - ftpData.TransferDetails.Socket?.Dispose(); - - var portArgs = request.Arguments[0].Split(','); - - ftpData.TransferDetails.Mode = TransferDetails.TransferMode.Active; - byte[] addressBytes = { byte.Parse(portArgs[0]), byte.Parse(portArgs[1]), byte.Parse(portArgs[2]), byte.Parse(portArgs[3]) }; - byte[] portBytes = { byte.Parse(portArgs[4]), byte.Parse(portArgs[5]) }; - - if (!BitConverter.IsLittleEndian) - { - Array.Reverse(portBytes); - } - - var address = new IPAddress(addressBytes); - var port = BitConverter.ToUInt16(portBytes, 0); - - ftpData.TransferDetails.DestinationDataAddress = address; - ftpData.TransferDetails.DestinationDataPort = port; - - ftpData.TransferDetails.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "PORT Command Accepted" }; - } - - private FtpResponse EnablePassiveMode(FtpRequest request, FtpData ftpData, ClientData client) - { - ftpData.TransferDetails.Socket?.Dispose(); - - ftpData.TransferDetails.Mode = TransferDetails.TransferMode.Passive; - ftpData.TransferDetails.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - ftpData.TransferDetails.Socket.Bind(new IPEndPoint(IPAddress.Any, 0)); - byte[] address = ((IPEndPoint)client.TcpClient.Client.LocalEndPoint).Address.GetAddressBytes(); - byte[] portBytes = BitConverter.GetBytes((short)ftpData.TransferDetails.LocalDataPort); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(portBytes); - } - - ftpData.TransferDetails.Socket.Listen(10); - - return new FtpResponse - { - StatusCode = FtpResponseCodes.EnterPassiveMode, - Message = $"Entering Passive mode ({address[0]},{address[1]},{address[2]},{address[3]},{portBytes[0]},{portBytes[1]})" - }; - } - - private FtpResponse ListFolder(FtpRequest request, FtpData ftpData) - { - var path = GetPathInfo(ftpData, request.Arguments.Length > 0 ? request.Arguments[0] : ftpData.CurrentDirectory); - if (path != null) - { - IEnumerable directories = Directory.EnumerateDirectories(path); - IEnumerable files = Directory.EnumerateFiles(path); - - foreach (string dir in directories) - { - DateTime editDate = Directory.GetLastWriteTime(dir); - - string date = editDate < DateTime.Now.Subtract(TimeSpan.FromDays(180)) ? - editDate.ToString("MMM dd yyyy", CultureInfo.InvariantCulture) : - editDate.ToString("MMM dd HH:mm", CultureInfo.InvariantCulture); - - var data = "drwxr-xr-x 2 2003 2003 4096 " + date + ' ' + Path.GetFileName(dir); - var bytes = Encoding.UTF8.GetBytes(data); - ftpData.SendData(bytes, bytes.Length); - } - - foreach (string file in files) - { - FileInfo f = new FileInfo(file); - - string date = f.LastWriteTime < DateTime.Now.Subtract(TimeSpan.FromDays(180)) ? - f.LastWriteTime.ToString("MMM dd yyyy", CultureInfo.InvariantCulture) : - f.LastWriteTime.ToString("MMM dd HH:mm", CultureInfo.InvariantCulture); - - var line = "-rw-r--r-- 2 2003 2003 "; - - var length = f.Length.ToString(CultureInfo.InvariantCulture); - - if (length.Length < 8) - { - for (int i = 0; i < 8 - length.Length; i++) - { - line += ' '; - } - } - - line += length + ' ' + date + ' ' + f.Name; - var bytes = Encoding.UTF8.GetBytes(line); - ftpData.SendData(bytes, bytes.Length); - } - } - return new FtpResponse { StatusCode = FtpResponseCodes.ClosingDataConnection, Message = "Transfer complete" }; - } - - private FtpResponse HandleTypeArguments(FtpRequest request, FtpData ftpData) - { - if (request.Arguments[0] == "I") - { - ftpData.TransferDetails.Type = TransferDetails.TransferType.BINARY; - return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "Set transfer encoding to binary" }; - } - else if (request.Arguments[0] == "A") - { - ftpData.TransferDetails.Type = TransferDetails.TransferType.ASCII; - return new FtpResponse { StatusCode = FtpResponseCodes.ActionCompleted, Message = "Set transfer encoding to ASCII" }; - } - else - { - return new FtpResponse { StatusCode = FtpResponseCodes.CommandNotImplementedForParameter, Message = $"Command not implemented for parameter {request.Arguments[0]}" }; - } - } - - private FtpResponse PrintCurrentDirectory(FtpRequest request, FtpData ftpData) - { - var relativePath = ftpData.CurrentDirectory.RelativePath(this.RootPath); - if (string.IsNullOrWhiteSpace(relativePath)) - { - relativePath = "\\"; - } - return new FtpResponse { StatusCode = FtpResponseCodes.PATHNAMECreated, Message = $"\"{relativePath}\" is current directory" }; - } - - private string GetPathInfo(FtpData ftpData, string path) - { - if (path == null) - { - path = string.Empty; - } - - if (path == "/") - { - return this.RootPath; - } - else if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - { - path = new FileInfo(Path.Combine(this.RootPath, path.Substring(1))).FullName; - } - else - { - path = new FileInfo(Path.Combine(ftpData.CurrentDirectory, path)).FullName; - } - - return path.IsSubPathOf(this.RootPath) ? path : null; - } - } -}