mirror of
https://github.com/AlexMacocian/MTSC.git
synced 2026-07-15 14:59:33 +00:00
Base implementation for FtpHandler
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,6 +7,12 @@ namespace MTSC.Common.Ftp.FtpModules
|
||||
public class AuthenticationModule : IFtpModule
|
||||
{
|
||||
public Func<AuthenticationData, bool> ValidateAuthentication { get; set; } = AlwaysAllowed;
|
||||
|
||||
public AuthenticationModule WithAuthenticationValidation(Func<AuthenticationData, bool> func)
|
||||
{
|
||||
this.ValidateAuthentication = func;
|
||||
return this;
|
||||
}
|
||||
bool IFtpModule.HandleRequest(FtpRequest request, ClientData client, FtpHandler handler, Server server)
|
||||
{
|
||||
if (!client.Resources.TryGetResource<AuthenticationData>(out var authData))
|
||||
|
||||
@@ -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<FtpData>(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]}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FtpData>(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<string> directories = Directory.EnumerateDirectories(path);
|
||||
IEnumerable<string> 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!" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FtpData>(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!" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FtpData>(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<string> directories = Directory.EnumerateDirectories(path);
|
||||
IEnumerable<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user