Initial commit.

This commit is contained in:
2019-07-17 21:01:14 +02:00
parent 528d83d6b6
commit db87c14a75
37 changed files with 2261 additions and 3 deletions

3
.gitignore vendored
View File

@@ -337,4 +337,5 @@ ASALocalRun/
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
healthchecksdb
/.vscode

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using YamlDotNet.Serialization;
namespace DearFTP.Configurations
{
class Configuration
{
public const string ConfigurationPath = "config.yml";
public static Configuration Load()
{
Configuration configuration;
if (File.Exists(ConfigurationPath))
{
var deserializer = new DeserializerBuilder().Build();
configuration = deserializer.Deserialize<Configuration>(File.ReadAllText(ConfigurationPath));
}
else
{
configuration = new Configuration();
configuration.Save();
}
return configuration;
}
public ServerConfiguration Server { get; set; } = new ServerConfiguration();
public Share[] Shares { get; set; } = Array.Empty<Share>();
public User[] Users { get; set; } = Array.Empty<User>();
public Configuration()
{
}
public bool Check()
{
foreach (var share in Shares)
{
share.Name = Path.GetFileName(share.Path);
if (!Directory.Exists(share.Path))
{
Console.WriteLine($"Path does not exist for '{share.Name}' share!");
return false;
}
}
if (!string.IsNullOrWhiteSpace(Server.Guest) && Users.Count(x => x.Name == Server.Guest) != 1)
{
Console.WriteLine($"Guest user does not exist: {Server.Guest}");
return false;
}
return true;
}
public void Save()
{
var serializer = new SerializerBuilder().EmitDefaults().Build();
File.WriteAllText(ConfigurationPath, serializer.Serialize(this));
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Configurations
{
class ServerConfiguration
{
public ushort Port { get; set; } = 21;
public string MOTD { get; set; } = "DearFTP v0.1";
public string LoginMessage { get; set; } = "Logged in as %user%";
public bool LogFileEnabled { get; set; } = false;
public string LogFilePath { get; set; } = "log.txt";
public string Guest { get; set; } = "";
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
using YamlDotNet.Serialization;
namespace DearFTP.Configurations
{
class Share
{
[YamlIgnore]
public string Name { get; set; }
public string Path { get; set; }
public string Group { get; set; }
public Share()
{
}
public Share(string name, string path, string group)
{
Name = name;
Path = path;
Group = group;
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Configurations
{
class User
{
public string Name { get; set; }
public string Password { get; set; }
public string[] Groups { get; set; }
public User()
{
}
public User(string name, string password, string[] groups)
{
Name = name;
Password = password;
Groups = groups;
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ClntCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"CLNT"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send(ResponseCode.OK, "OK");
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class CommandsDispatcher
{
public ICommand[] Commands { get; } = new ICommand[]
{
new ClntCommand(),
new CwdCommand(),
new DeleteCommand(),
new HelpCommand(),
new FeaturesCommand(),
new FileModificationTimeCommand(),
new ListCommand(),
new ListMachineCommand(),
new MakeDirectoryCommand(),
new ParentDirectoryCommand(),
new PassiveCommand(),
new PwdCommand(),
new QuitCommand(),
new RenameCommand(),
new RetrieveCommand(),
new SiteCommand(),
new SizeCommand(),
new StoreCommand(),
new SystemCommand(),
new TypeCommand(),
new UserCommand()
};
public void Dispatch(Session session, string command, string argument)
{
if (command == "END")
{
session.Stop();
return;
}
var commandExecutor = Commands.FirstOrDefault(x => x.Aliases.Contains(command, StringComparer.OrdinalIgnoreCase));
if (commandExecutor == null)
{
session.FtpStream.Send(ResponseCode.NotImplemented, $"Command '{command}' not implemented or invalid");
return;
}
commandExecutor.Execute(session, session.FtpStream, command, argument);
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class CwdCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"CWD",
"XCWD"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (session.NavigablePath.NavigateTo(argument))
{
stream.Send(ResponseCode.FileActionOK, "Directory successfully changed.");
}
else
{
stream.Send(ResponseCode.FileUnavailable, "Failed to change directory.");
}
}
}
}

View File

@@ -0,0 +1,70 @@
using DearFTP.Configurations;
using DearFTP.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class DeleteCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"DELE",
"RMD",
"XRMD"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
(NavigablePath navigablePath, string path, bool isDirectory) = session.NavigablePath.GetSystemFilePath(argument);
if (navigablePath == null)
{
stream.Send(ResponseCode.FileUnavailable, "File does not exist.");
return;
}
if (navigablePath.CurrentDirectory == "/")
{
stream.Send(ResponseCode.FileUnavailable, "Operation not allowed: can't remove a share.");
return;
}
if (!session.WritablesShares.Contains(navigablePath.CurrentShare))
{
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
return;
}
if (isDirectory)
{
if (alias.ToUpper() == "RMD")
{
Directory.Delete(path, true);
stream.Send(ResponseCode.FileActionOK, "Directory successfully deleted.");
}
else
{
stream.Send(ResponseCode.ArgumentsError, "Path is a directory and not a file.");
}
}
else
{
if (alias.ToUpper() == "DELE")
{
File.Delete(path);
stream.Send(ResponseCode.FileActionOK, "File successfully deleted");
}
else
{
stream.Send(ResponseCode.ArgumentsError, "Path is a file and not a directory.");
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class FeaturesCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"FEAT",
"FEATURES"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send
(
ResponseCode.SystemStatusOrHelpReply,
"Features:",
"MDTM",
"MLST",
"PASV",
"REST STREAM",
"SIZE",
"TVFS",
"UTF8"
);
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class FileModificationTimeCommand : ICommand
{
public const string DateFormat = "yyyyMMddhhmmss";
public string[] Aliases { get; } = new string[]
{
"MDTM"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (string.IsNullOrWhiteSpace(argument))
{
stream.Send(ResponseCode.ArgumentsError, "Path can't be empty.");
return;
}
var split = argument.Split(' ');
if (split.Length > 1 && DateTime.TryParseExact(split[0], DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date))
{
ModifyTime(session, stream, date, string.Join(' ', split.Skip(1)));
}
else
{
SendTime(session, stream, argument);
}
}
private static void SendTime(Session session, FtpStream stream, string argument)
{
var (_, realPath, isDirectory) = session.NavigablePath.GetSystemFilePath(argument);
if (realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file does not exist.");
return;
}
FileSystemInfo info = isDirectory ? new DirectoryInfo(realPath) : (FileSystemInfo)new FileInfo(realPath);
stream.Send(ResponseCode.FileStatus, info.LastWriteTimeUtc.ToString(DateFormat));
}
private void ModifyTime(Session session, FtpStream stream, DateTime date, string path)
{
var file = session.NavigablePath.GetSystemFilePath(path);
if (file.realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file does not exist.");
return;
}
FileSystemInfo info = file.isDirectory ? new DirectoryInfo(file.realPath) : (FileSystemInfo)new FileInfo(file.realPath);
info.LastWriteTimeUtc = date;
stream.Send(ResponseCode.OK, "Modification date successfully changed.");
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class HelpCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"HELP"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (argument.ToUpper() == "SITE")
{
stream.Send
(
ResponseCode.HelpMessage,
"Commands:",
"ABOR",
"ALLO",
"APPE",
"CDUP",
"CWD",
"DELE",
"FEAT",
"HELP",
"LIST",
"MDTM",
"MKD",
"MLST",
"MLSD",
"MODE",
"NLST",
"NOOP",
"OPTS",
"PASS",
"PASV",
"PORT",
"PWD",
"QUIT",
"REIN",
"REST",
"RETR",
"RMD",
"RNFR",
"RNTO",
"SITE",
"SIZE",
"STAT",
"STOR",
"STOU",
"STRU",
"SYST",
"TYPE",
"USER",
"XCUP",
"XCWD",
"XMKD",
"XPWD",
"XRMD"
);
}
else
{
stream.Send(ResponseCode.ArgumentNotImplemented, "Wrong argument");
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
interface ICommand
{
string[] Aliases { get; }
void Execute(Session session, FtpStream stream, string alias, string argument);
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ListCommand : ICommand
{
public const int BufferSize = 4096;
public string[] Aliases { get; } = new string[]
{
"LIST",
"NLST"
};
public string[] SizeModifiers { get; } = new string[] { "", "K", "M", "G", "T", "P" };
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
stream.Send(ResponseCode.FileStatusOK, "Listing coming.");
string path = null;
bool humanReadable = false;
foreach (string arg in argument.Split(' '))
{
if (arg.StartsWith('-') && arg.ToLower().Contains('h'))
{
humanReadable = true;
continue;
}
if (arg.StartsWith('-'))
{
continue;
}
path = arg;
}
SendList(session, path ?? ".", humanReadable);
dataConnection.Close();
stream.Send(ResponseCode.CloseDataConnection, "Listing complete.");
}
private void SendList(Session session, string path, bool humanReadable)
{
var files = new Stack<string>(GenerateFilesInfo(session, path, humanReadable).Reverse());
var stream = new FtpStream(session.DataConnection.Stream);
var builder = new StringBuilder(BufferSize);
while (files.Count > 0)
{
string file = files.Pop();
if (builder.Length + file.Length < BufferSize)
{
builder.Append(file);
}
else
{
files.Push(file);
stream.Send(builder.ToString(), false);
builder = new StringBuilder(BufferSize);
}
}
if (builder.Length > 0)
{
stream.Send(builder.ToString(), false);
}
}
private IEnumerable<string> GenerateFilesInfo(Session session, string path, bool humanReadable)
{
bool writeable = session.WritablesShares.Contains(session.NavigablePath.CurrentShare);
yield return GenerateFileInfo(true, writeable, "DearFTP", "DearFTP", 0L, DateTime.Now, ".", humanReadable);
yield return GenerateFileInfo(true, writeable, "DearFTP", "DearFTP", 0L, DateTime.Now, "..", humanReadable);
foreach (string directory in session.NavigablePath.GetDirectories(path))
{
var directoryInfo = new DirectoryInfo(directory);
yield return GenerateFileInfo(true, writeable, "DearFTP", "DearFTP", 0L, directoryInfo.LastWriteTime, directoryInfo.Name, humanReadable);
}
foreach (string file in session.NavigablePath.GetFiles(path))
{
var fileInfo = new FileInfo(file);
yield return GenerateFileInfo(false, writeable, "DearFTP", "DearFTP", fileInfo.Length, fileInfo.LastWriteTime, fileInfo.Name, humanReadable);
}
}
private string GenerateFileInfo(bool isDirectory, bool isWritable, string owner, string group, long size, DateTime lastModification, string name, bool humanReadableSize)
{
string permissions = isDirectory ? (isWritable ? "drwxr-xr-x" : "dr-xr-xr-x") : (isWritable ? "-rw-r--r--" : "-r--r--r--");
string date = (DateTime.Now - lastModification).TotalDays > 365 ? lastModification.ToString("MMM dd yyyy", CultureInfo.InvariantCulture) : lastModification.ToString("MMM dd hh:mm", CultureInfo.InvariantCulture);
return $"{permissions} 1 {owner} {group}{(humanReadableSize ? GenerateSize(size) : size.ToString()),13} {date,-13}{name}\r\n";
}
private string GenerateSize(long size)
{
double newSize = size;
foreach (string modifier in SizeModifiers)
{
if (newSize < 1000)
{
if (newSize < 10)
{
return $"{newSize:0.#}{modifier}";
}
return $"{(int)newSize}{modifier}";
}
else
{
newSize /= 1000;
}
}
throw new ArgumentOutOfRangeException("size", "Can't format 'size', it is too big.");
}
}
}

View File

@@ -0,0 +1,144 @@
using DearFTP.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ListMachineCommand : ICommand
{
public const string DateFormat = "yyyyMMddhhmmss";
public string[] Aliases { get; } = new string[]
{
"MLST",
"MLSD"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
switch (alias.ToUpper())
{
case "MLST":
ListSingleObject(session, stream, argument);
break;
case "MLSD":
ListMultipleObject(session, stream, argument);
break;
}
}
private void ListSingleObject(Session session, FtpStream stream, string argument)
{
var (navigablePath, realPath, isDirectory) = session.NavigablePath.GetSystemFilePath(argument);
if (realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file does not exist.");
return;
}
FileSystemInfo info = isDirectory ? new DirectoryInfo(realPath) : (FileSystemInfo)new FileInfo(realPath);
bool writeable = session.WritablesShares.Contains(navigablePath.CurrentShare);
string line = GenerateInfo(info, writeable);
stream.Send(ResponseCode.FileActionOK, "Listing.", line);
}
private void ListMultipleObject(Session session, FtpStream stream, string argument)
{
var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
var navigablePath = new NavigablePath(session.NavigablePath);
if (!navigablePath.NavigateTo(argument))
{
stream.Send(ResponseCode.FileUnavailable, "Invalid path.");
}
bool writeable = session.WritablesShares.Contains(navigablePath.CurrentShare);
var infos = GenerateFilesInfo(navigablePath, writeable);
stream.Send(ResponseCode.FileStatusOK, "Listing coming.");
var dataStream = new FtpStream(dataConnection.Stream);
foreach (var info in infos)
{
dataStream.Send(info);
}
dataConnection.Close();
stream.Send(ResponseCode.CloseDataConnection, "Listing complete.");
}
private string GenerateInfo(FileSystemInfo fileSystemInfo, bool writeable, string specialType = null)
{
string date = fileSystemInfo.LastWriteTimeUtc.ToString(DateFormat);
if (fileSystemInfo is DirectoryInfo directoryInfo)
{
string type = specialType ?? "dir";
string permissions = $"el{(writeable ? "dfcmp" : "")}";
return $"Type={type};Modify={date};Perm={permissions}; {directoryInfo.Name}";
}
else if (fileSystemInfo is FileInfo fileInfo)
{
string type = specialType ?? "file";
string permissions = $"r{(writeable ? "dfaw" : "")}";
return $"Type={type};Modify={date};Perm={permissions};Size={fileInfo.Length}; {fileInfo.Name}";
}
return "";
}
private IEnumerable<string> GenerateFilesInfo(NavigablePath navigablePath, bool writeable)
{
string virtualDirectory = navigablePath.CurrentDirectory;
if (virtualDirectory != "/")
{
string currentDirectory = navigablePath.GetDirectoryPath("");
yield return GenerateInfo(new DirectoryInfo(currentDirectory), writeable, "cdir");
if (virtualDirectory.Count(x => x == '/') > 1)
{
yield return GenerateInfo(new DirectoryInfo(currentDirectory).Parent, writeable, "pdir");
}
}
else
{
virtualDirectory = "";
}
foreach (var directory in navigablePath.GetDirectories())
{
var directoryInfo = new DirectoryInfo(directory);
yield return GenerateInfo(directoryInfo, writeable);
}
foreach (var file in navigablePath.GetFiles())
{
var fileInfo = new FileInfo(file);
yield return GenerateInfo(fileInfo, writeable);
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class MakeDirectoryCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"MKD",
"XMKD"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (session.NavigablePath.GetSystemFilePath(argument).realPath != null)
{
stream.Send(ResponseCode.FileUnavailable, "Directory already exists.");
return;
}
var newDirectory = session.NavigablePath.GetNewSystemFilePath(argument);
if (newDirectory.navigablePath.CurrentDirectory == "/")
{
stream.Send(ResponseCode.FileUnavailable, "Can't modify the root.");
return;
}
if (newDirectory.realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Invalid destination path.");
return;
}
if (File.Exists(newDirectory.realPath) || Directory.Exists(newDirectory.realPath))
{
stream.Send(ResponseCode.FileUnavailable, "Destination path already exists.");
return;
}
if (!session.WritablesShares.Contains(newDirectory.navigablePath.CurrentShare))
{
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
return;
}
Directory.CreateDirectory(newDirectory.realPath);
stream.Send(ResponseCode.FileActionOK, "Directory successfully created.");
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class ParentDirectoryCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"CDUP",
"XCUP"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (session.NavigablePath.NavigateTo(".."))
{
stream.Send(ResponseCode.FileActionOK, "Directory successfully changed.");
}
else
{
stream.Send(ResponseCode.FileUnavailable, "Failed to change directory.");
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace DearFTP.Connection.Commands
{
class PassiveCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"PASV"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
session.DataConnection.Create();
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));
stream.Send(ResponseCode.PassiveMode, $"Entering Passive Mode ({remote})");
session.DataConnection.AcceptClient();
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class PwdCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"PWD",
"XPWD"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send(ResponseCode.PathNameCreated, $"\"{session.CurrentWorkingDirectory}\" is the current working directory");
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class QuitCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"QUIT",
"EXIT"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send(ResponseCode.ServiceClosingConnection, "Goodbye.");
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class RenameCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"RNFR"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
var sourceFile = session.NavigablePath.GetSystemFilePath(argument);
if (sourceFile.navigablePath == null)
{
stream.Send(ResponseCode.FileUnavailable, "File does not exist.");
return;
}
if (sourceFile.navigablePath.CurrentDirectory == "/")
{
stream.Send(ResponseCode.FileUnavailable, "Operation not allowed: can't remove a share.");
return;
}
if (!session.WritablesShares.Contains(sourceFile.navigablePath.CurrentShare))
{
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
return;
}
stream.Send(ResponseCode.PendingFurtherInformation, "Waiting destination.");
(string command, string destination) = stream.Receive();
if (command.ToUpper() != "RNTO")
{
stream.Send(ResponseCode.BadSequence, "Expected a RNTO command.");
return;
}
var destinationPath = session.NavigablePath.GetNewSystemFilePath(destination);
if (destinationPath.realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Invalid destination path.");
return;
}
if (File.Exists(destinationPath.realPath) || Directory.Exists(destinationPath.realPath))
{
stream.Send(ResponseCode.FileUnavailable, "Destination path already exists.");
return;
}
if (!session.WritablesShares.Contains(destinationPath.navigablePath.CurrentShare))
{
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
return;
}
if (sourceFile.isDirectory)
{
Directory.Move(sourceFile.realPath, destinationPath.realPath);
}
else
{
File.Move(sourceFile.realPath, destinationPath.realPath);
}
stream.Send(ResponseCode.FileActionOK, "File successfully renamed.");
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace DearFTP.Connection.Commands
{
class RetrieveCommand : ICommand
{
public const int BufferSize = 4096;
public string[] Aliases { get; } = new string[]
{
"RETR"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
var (_, realPath, isDirectory) = session.NavigablePath.GetSystemFilePath(argument);
if (realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file does not exist.");
return;
}
if (isDirectory)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file is a directory.");
return;
}
stream.Send(ResponseCode.FileStatusOK, "File coming.");
SendFile(dataConnection.Stream, realPath);
dataConnection.Close();
stream.Send(ResponseCode.CloseDataConnection, "File sent.");
}
private void SendFile(Stream stream, string path)
{
using (var file = File.OpenRead(path))
{
Span<byte> buffer = stackalloc byte[BufferSize];
long bytesToWrite = Math.Min(file.Length - file.Position, BufferSize);
while (file.Read(buffer) > 0)
{
if (bytesToWrite < BufferSize)
{
stream.Write(buffer.Slice(0, (int)bytesToWrite));
break;
}
stream.Write(buffer);
bytesToWrite = Math.Min(file.Length - file.Position, BufferSize);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class SiteCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"SITE"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send(ResponseCode.NotImplemented, "SITE command not implemented.");
}
}
}

View File

@@ -0,0 +1,37 @@
using DearFTP.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace DearFTP.Connection.Commands
{
class SizeCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"SIZE"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
if (string.IsNullOrWhiteSpace(argument))
{
stream.Send(ResponseCode.ArgumentsError, "Path can't be empty.");
return;
}
var file = session.NavigablePath.GetSystemFilePath(argument);
if (file.realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Requested file does exist.");
return;
}
long size = file.isDirectory ? 4096L : new FileInfo(file.realPath).Length;
stream.Send(ResponseCode.FileStatus, size.ToString());
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
namespace DearFTP.Connection.Commands
{
class StoreCommand : ICommand
{
public const int BufferSize = 4096;
public string[] Aliases { get; } = new string[]
{
"STOR"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
var dataConnection = session.DataConnection;
if (!dataConnection.IsAvailable)
{
stream.Send(ResponseCode.DataConnectionOpenError, "Passive mode not activated.");
return;
}
var (navigablePath, realPath) = session.NavigablePath.GetNewSystemFilePath(argument);
if (realPath == null)
{
stream.Send(ResponseCode.FileUnavailable, "Invalid destination path.");
return;
}
if (Directory.Exists(realPath))
{
stream.Send(ResponseCode.FileUnavailable, "A directory with this name already exists.");
return;
}
if (!session.WritablesShares.Contains(navigablePath.CurrentShare))
{
stream.Send(ResponseCode.FileUnavailable, "You don't have write access to this file.");
return;
}
stream.Send(ResponseCode.FileStatusOK, "Waiting file.");
ReceiveFile(dataConnection.Stream, realPath);
dataConnection.Close();
stream.Send(ResponseCode.CloseDataConnection, "File received.");
}
private void ReceiveFile(NetworkStream stream, string path)
{
using (var file = File.Open(path, FileMode.Create))
{
Span<byte> buffer = stackalloc byte[BufferSize];
int readBytes;
while ((readBytes = stream.Read(buffer)) > 0)
{
if (readBytes < BufferSize)
{
file.Write(buffer.Slice(0, readBytes));
}
else
{
file.Write(buffer);
}
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class SystemCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"SYST"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
stream.Send(ResponseCode.NameSystemType, "UNIX Type: L8");
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection.Commands
{
class TypeCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"TYPE"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
// Is there something to do here?
stream.Send(ResponseCode.OK, "Type changed.");
}
}
}

View File

@@ -0,0 +1,60 @@
using DearFTP.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DearFTP.Connection.Commands
{
class UserCommand : ICommand
{
public string[] Aliases { get; } = new string[]
{
"USER"
};
public void Execute(Session session, FtpStream stream, string alias, string argument)
{
var configuration = FtpServer.Instance.Configuration;
var user = configuration.Users.FirstOrDefault(x => x.Name == argument);
if (user == null)
{
string guest = configuration.Server.Guest;
if (string.IsNullOrWhiteSpace(guest))
{
stream.Send(ResponseCode.InvalidCreditentials, "Invalid username.");
session.Stop();
return;
}
user = configuration.Users.First(x => x.Name == guest);
}
if (!string.IsNullOrWhiteSpace(user.Password))
{
stream.Send(ResponseCode.NeedPassword, "Please specify the password.");
(_, string pass) = stream.Receive();
string hash = PasswordHash.GetHash(pass);
if (user.Password != hash)
{
stream.Send(ResponseCode.InvalidCreditentials, "Invalid password.");
session.Stop();
return;
}
}
session.User = user;
session.WritablesShares = configuration.Shares.Where(s => user.Groups.Contains($"+{s.Group}")).ToArray();
session.Shares = session.WritablesShares.Concat(configuration.Shares.Where(s => user.Groups.Contains(s.Group))).ToArray();
session.NavigablePath = new NavigablePath(session.Shares);
session.Logger.Log($"Logged in as {user.Name}.");
stream.Send(ResponseCode.LoggedIn, configuration.Server.LoginMessage.Replace("%user%", user.Name));
}
}
}

View File

@@ -0,0 +1,59 @@
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();
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
namespace DearFTP.Connection
{
class FtpStream
{
public const int BUFFER_SIZE = 4096;
public NetworkStream NetworkStream { get; }
public FtpStream(NetworkStream networkStream)
{
NetworkStream = networkStream;
}
public (string command, string argument) Receive()
{
var buffer = new byte[BUFFER_SIZE];
int readBytes = NetworkStream.Read(buffer, 0, buffer.Length);
if (readBytes == 0)
{
return ("END", "");
}
string packet = Encoding.UTF8.GetString(buffer, 0, readBytes);
if (!packet.EndsWith("\r\n"))
{
throw new Exception("Ftp packet is in wrong format");
}
packet = packet.Remove(packet.Length - 2);
string[] split = packet.Split(' ');
string command = split[0];
string argument = string.Join(' ', split.Skip(1));
return (command, argument);
}
public void Send(string message, bool end = true)
{
var bytes = Encoding.UTF8.GetBytes($"{message}{(end ? "\r\n" : "")}");
NetworkStream.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);
}
public void Send(ResponseCode code, string message, params string[] arguments)
{
var builder = new StringBuilder();
builder.Append($"{(uint)code}-{message}\r\n");
foreach (string argument in arguments)
{
builder.Append($" {argument}\r\n");
}
builder.Append($"{(uint)code} End\r\n");
var bytes = Encoding.UTF8.GetBytes(builder.ToString());
NetworkStream.Write(bytes, 0, bytes.Length);
}
}
}

View File

@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DearFTP.Connection
{
public enum ResponseCode : uint
{
// Source: https://www.smartfile.com/blog/big-list-ftp-server-response-codes/
// 1xx The request has started, expect another reply before proceeding with a new command.
/// <summary>
/// Restart marker replay. In this case, the text is exact and not left to the particular implementation;
/// it must read: MARK yyyy = mmmm where yyyy is User-process data stream marker, and mmmm servers equivalent marker (note the spaces between markers and “=”).
/// </summary>
RestartMarkerReplay = 110,
/// <summary>
/// Service ready in xx minutes.
/// </summary>
ServiceReadyIn = 120,
/// <summary>
/// Data connection is already open and the transfer is starting.
/// </summary>
DataConnectionAlreadyOpen = 125,
/// <summary>
/// File status is okay and about to open data connection.
/// </summary>
FileStatusOK = 150,
// 2xx The request was successfully completed.
/// <summary>
/// A server issues a 200 response code if a command is accepted and successfully processed.
/// </summary>
OK = 200,
/// <summary>
/// Command was not implemented, superfluous at this site.
/// </summary>
CommandNotImplemented = 202,
/// <summary>
/// System status, or system help reply.
/// </summary>
SystemStatusOrHelpReply = 211,
/// <summary>
/// Directory status.
/// </summary>
DirectoryStatus = 212,
/// <summary>
/// File status.
/// </summary>
FileStatus = 213,
/// <summary>
/// Help message. On how to use the server or the meaning of a particular non-standard command.
/// </summary>
HelpMessage = 214,
/// <summary>
/// NAME system type. Where NAME is an official system name from the registry kept by IANA.
/// </summary>
NameSystemType = 215,
/// <summary>
/// Service is ready for new user.
/// </summary>
ServiceReady = 220,
/// <summary>
/// Service closing control connection.
/// </summary>
ServiceClosingConnection = 221,
/// <summary>
/// Data connection is open and no transfer is in progress.
/// </summary>
DataConnectionOpenNoTransfer = 225,
/// <summary>
/// Closing the data connection. Requested file action successful (for example, file transfer or file abort).
/// </summary>
CloseDataConnection = 226,
/// <summary>
/// Entering Passive Mode (h1, h2, h3, h4, p1, p2).
/// </summary>
PassiveMode = 227,
/// <summary>
/// Entering Long Passive Mode (long address, port).
/// </summary>
LongPassiveMode = 228,
/// <summary>
/// Entering Extended Passive Mode (|||port|).
/// </summary>
ExtendedPassiveMode = 229,
/// <summary>
/// User has logged in, proceed. Logged out if appropriate.
/// </summary>
LoggedIn = 230,
/// <summary>
/// User has logged out and the service is terminated.
/// </summary>
LoggedOut = 231,
/// <summary>
/// Logout command noted, will complete when the transfer done.
/// </summary>
LogoutCommandOnEnd = 232,
/// <summary>
/// Specifies that the server accepts the authentication mechanism specified by the client, and the exchange of security data is complete. A higher level nonstandard code created by Microsoft.
/// </summary>
AcceptAuthenticationMechanism = 234,
/// <summary>
/// Requested file action okay and completed.
/// </summary>
FileActionOK = 250,
/// <summary>
/// “PATHNAME” created.
/// </summary>
PathNameCreated = 257,
// 3xx The command was accepted, but the request is on hold, pending receipt of further information.
/// <summary>
/// User name okay, need password.
/// </summary>
NeedPassword = 331,
/// <summary>
/// Need account for login.
/// </summary>
NeedAccount = 332,
/// <summary>
/// Requested file action pending further information
/// </summary>
PendingFurtherInformation = 350,
// 4xx The command wasnt accepted and the requested action didnt occur, but the error condition is temporary and the action may be requested again.
/// <summary>
/// Service not available, closing control connection. This may be a reply to any command if the service knows it must shut down.
/// </summary>
ServiceNotAvailable = 421,
/// <summary>
/// Cant open data connection.
/// </summary>
DataConnectionOpenError = 425,
/// <summary>
/// Connection closed; transfer aborted.
/// </summary>
ConnectionClosed = 426,
/// <summary>
/// Invalid username or password.
/// </summary>
InvalidCreditentials = 430,
/// <summary>
/// Requested host unavailable.
/// </summary>
HostUnavailable = 434,
/// <summary>
/// Requested file action not taken.
/// </summary>
FileActionNotTaken = 450,
/// <summary>
/// Requested action aborted. Local error in processing.
/// </summary>
ActionAborted = 451,
/// <summary>
/// Requested action not taken. Insufficient storage space in system.File unavailable (e.g., file busy).
/// </summary>
InsufficientStorage = 452,
// 5xx Syntax error, command unrecognized and the request did not take place. This may include errors such as command line too long.
/// <summary>
/// Syntax error in parameters or arguments.
/// </summary>
ArgumentsError = 501,
/// <summary>
/// Command not implemented.
/// </summary>
NotImplemented = 502,
/// <summary>
/// Bad sequence of commands.
/// </summary>
BadSequence = 503,
/// <summary>
/// Command not implemented for that parameter.
/// </summary>
ArgumentNotImplemented = 504,
/// <summary>
/// Not logged in.
/// </summary>
NotLoggedIn = 530,
/// <summary>
/// Need account for storing files.
/// </summary>
NeedAccountToStore = 532,
/// <summary>
/// Request not taken. File unavailable (e.g., file not found, no access).
/// </summary>
FileUnavailable = 550,
/// <summary>
/// Request aborted. Page type unknown.
/// </summary>
PageTypeUnknown = 551,
/// <summary>
/// Requested file action aborted. Exceeded storage allocation (for current directory or dataset).
/// </summary>
ExceededStorageAllocation = 552,
/// <summary>
/// Requested action not taken. File name not allowed.
/// </summary>
FileNameNotAllowed = 553,
// 6xx Replies regarding confidentiality and integrity
/// <summary>
/// Integrity protected reply.
/// </summary>
IntegrityProtectedReply = 631,
/// <summary>
/// Confidentiality and integrity protected reply.
/// </summary>
ConfidentialityAndIntegrityProtectedReply = 632,
/// <summary>
/// Confidentiality protected reply.
/// </summary>
ConfidentialityProtectedReply = 633
}
}

View File

@@ -0,0 +1,91 @@
using DearFTP.Configurations;
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.Sockets;
using System.Text;
namespace DearFTP.Connection
{
class Session : IDisposable
{
public Configuration Configuration { get; }
public CommandsDispatcher CommandsDispatcher { get; }
public Logger Logger { get; }
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 string CurrentWorkingDirectory => NavigablePath.CurrentDirectory;
public string IP => ((IPEndPoint)_client.Client.LocalEndPoint).Address.ToString();
public DataConnection DataConnection { get; set; }
private TcpClient _client;
private NetworkStream _networkStream;
private bool _isRunning = true;
public Session(TcpClient client)
{
_client = client;
_networkStream = client.GetStream();
FtpStream = new FtpStream(_networkStream);
Configuration = FtpServer.Instance.Configuration;
CommandsDispatcher = FtpServer.Instance.CommandsDispatcher;
Logger = FtpServer.Instance.Logger;
DataConnection = new DataConnection();
}
public void Start()
{
FtpStream.Send(ResponseCode.ServiceReady, Configuration.Server.MOTD);
while (_isRunning)
{
(string command, string argument) = FtpStream.Receive();
Logger.Log($"[{_client.Client.RemoteEndPoint}]: {command} {argument}");
CommandsDispatcher.Dispatch(this, command, argument);
}
}
public void Stop()
{
_isRunning = false;
Dispose();
}
public string GetRealPath(string path)
{
string completePath = Path.Combine(CurrentWorkingDirectory, path);
string[] split = CurrentWorkingDirectory.Substring(1).Split('/');
if (split.Length == 0)
{
return null;
}
var share = Shares.FirstOrDefault(x => x.Name == split[0]);
if (share == null)
{
return null;
}
return Path.Combine(split.Skip(1).Prepend(share.Path).ToArray());
}
public void Dispose()
{
_networkStream.Close();
_client.Close();
}
}
}

91
DearFTP/FtpServer.cs Normal file
View File

@@ -0,0 +1,91 @@
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.Connection;
using DearFTP.Connection.Commands;
namespace DearFTP
{
class FtpServer
{
public static FtpServer Instance { get; private set; }
public Configuration Configuration { get; private set; }
public Logger Logger { get; private set; }
public CommandsDispatcher CommandsDispatcher { get; }
private bool _isRunning = true;
private TcpListener _listener;
public FtpServer()
{
Instance = this;
CommandsDispatcher = new CommandsDispatcher();
}
public void Start()
{
Console.WriteLine("Starting DearFTP");
LoadConfig();
Logger = new Logger(Configuration);
Logger.Log("DearFTP started");
Logger.Log();
TcpLoop();
}
public void Stop()
{
_isRunning = false;
_listener.Stop();
}
private void TcpLoop()
{
_listener = new TcpListener(IPAddress.Any, Configuration.Server.Port);
_listener.Start(5);
Logger.Log($"Listening on: {_listener.LocalEndpoint}");
while (_isRunning)
{
var client = _listener.AcceptTcpClient();
Task.Run(() => CreateSession(client));
}
}
private void CreateSession(TcpClient client)
{
Logger.Log($"Client connected: {client.Client.RemoteEndPoint}");
using (var session = new Session(client))
{
session.Start();
}
}
private void LoadConfig()
{
Console.WriteLine("Loading config...");
Configuration = Configuration.Load();
if (!Configuration.Check())
{
Console.WriteLine("Error while loading configuration, exiting...");
Environment.Exit(1);
}
Console.WriteLine("Config loaded");
}
}
}

56
DearFTP/Logger.cs Normal file
View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using DearFTP.Configurations;
namespace DearFTP
{
class Logger
{
public bool WriteToFile { get; private set; }
public string FilePath { get; private set; }
public Logger(Configuration configuration)
{
WriteToFile = configuration.Server.LogFileEnabled;
FilePath = configuration.Server.LogFilePath;
}
public void Log(string message)
{
if (WriteToFile)
{
LogToFile(message);
}
Console.WriteLine(message);
}
public void Log()
{
if (WriteToFile)
{
LogToFile("");
}
Console.WriteLine();
}
public void LogError(string error, string description)
{
if (WriteToFile)
{
LogToFile($"[Error] {error}: {description}");
}
Console.Error.WriteLine($"[Error] {error}: {description}");
}
private void LogToFile(string content)
{
File.AppendAllText(FilePath, $"{content}\n");
}
}
}

View File

@@ -1,4 +1,10 @@
using System;
using DearFTP.Utils;
using System;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace DearFTP
{
@@ -6,7 +12,47 @@ namespace DearFTP
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
if (args.Length != 0)
{
ProceedArguments(args);
}
// Make sure to use unique format
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("Welcome to DearFTP !");
Console.WriteLine();
var server = new FtpServer();
Task.Run(() => server.Start());
while (Console.ReadLine() != "stop") { }
server.Stop();
Environment.Exit(0);
}
private static void ProceedArguments(string[] args)
{
string command = args[0].ToLower().Replace("-", "");
string[] arguments = args.Skip(1).ToArray();
if (command == "hash" && arguments.Length > 0)
{
foreach (string password in arguments)
{
Console.WriteLine(PasswordHash.GetHash(password));
}
}
else
{
Console.WriteLine("Invalid usage.");
Environment.Exit(1);
}
Environment.Exit(0);
}
}
}

View File

@@ -0,0 +1,275 @@
using DearFTP.Configurations;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DearFTP.Utils
{
class NavigablePath
{
public Share[] Shares { get; }
public Share CurrentShare { get; set; }
public string CurrentDirectory { get; private set; }
private Stack<string> _path;
public NavigablePath(Share[] shares)
{
Shares = shares;
CurrentDirectory = "/";
_path = new Stack<string>();
}
public NavigablePath(NavigablePath navigablePath)
{
Shares = navigablePath.Shares;
CurrentShare = navigablePath.CurrentShare;
CurrentDirectory = navigablePath.CurrentDirectory;
_path = new Stack<string>(navigablePath._path);
}
public string GetFilePath(string fileName)
{
string path = Path.Combine(GetRealPath(), fileName);
if (File.Exists(path))
{
return path;
}
else
{
return null;
}
}
public string GetFilePath(string path, string fileName)
{
var tempNavigablePath = new NavigablePath(this);
if (tempNavigablePath.NavigateTo(path))
{
return tempNavigablePath.GetFilePath(fileName);
}
else
{
return null;
}
}
public string GetDirectoryPath(string directoryName)
{
string path = Path.Combine(GetRealPath(), directoryName);
if (Directory.Exists(path))
{
return path;
}
else
{
return null;
}
}
public string GetDirectoryPath(string path, string directoryName)
{
var tempNavigablePath = new NavigablePath(this);
if (tempNavigablePath.NavigateTo(path))
{
return tempNavigablePath.GetDirectoryPath(directoryName);
}
else
{
return null;
}
}
public (NavigablePath navigablePath, string realPath, bool isDirectory) GetSystemFilePath(string path)
{
var tempNavigablePath = new NavigablePath(this);
var split = path.Split('/');
if (split.Length > 1)
{
string workingPath = string.Join('/', split.SkipLast(1));
tempNavigablePath.NavigateTo(workingPath);
}
string name = split.Last();
if (tempNavigablePath._path.Count < 1)
{
return (tempNavigablePath, null, false);
}
if (tempNavigablePath.GetDirectoryPath(name) is string directoryPath)
{
return (tempNavigablePath, directoryPath, true);
}
else if (tempNavigablePath.GetFilePath(name) is string filePath)
{
return (tempNavigablePath, filePath, false);
}
return (null, null, false);
}
public (NavigablePath navigablePath, string realPath) GetNewSystemFilePath(string path)
{
var tempNavigablePath = new NavigablePath(this);
var split = path.Split('/');
if (split.Length > 1)
{
string workingPath = string.Join('/', split.SkipLast(1));
if (!tempNavigablePath.NavigateTo(workingPath))
{
return (null, null);
}
}
if (tempNavigablePath._path.Count < 1)
{
return (tempNavigablePath, null);
}
return (tempNavigablePath, Path.Combine(GetRealPath(), split.Last()));
}
public IEnumerable<string> GetDirectories()
{
if (_path.Count < 1)
{
return Shares.Select(x => x.Path);
}
string path = GetRealPath();
return Directory.EnumerateDirectories(path);
}
public IEnumerable<string> GetDirectories(string path)
{
var tempNavigablePath = new NavigablePath(this);
if (tempNavigablePath.NavigateTo(path))
{
return tempNavigablePath.GetDirectories();
}
else
{
return Enumerable.Empty<string>();
}
}
public IEnumerable<string> GetFiles()
{
if (_path.Count < 1)
{
return Enumerable.Empty<string>();
}
string path = GetRealPath();
return Directory.EnumerateFiles(path);
}
public IEnumerable<string> GetFiles(string path)
{
var tempNavigablePath = new NavigablePath(this);
if (tempNavigablePath.NavigateTo(path))
{
return tempNavigablePath.GetFiles();
}
else
{
return Enumerable.Empty<string>();
}
}
public bool NavigateTo(string path)
{
var oldPath = new Stack<string>(_path);
var queue = new Queue<string>(path.Split('/'));
// If absolute path
if (path.StartsWith('/'))
{
queue.Dequeue();
_path.Clear();
}
if (NavigateTo(queue))
{
CurrentDirectory = $"/{string.Join('/', _path)}";
return true;
}
else
{
_path = oldPath;
return false;
}
}
private bool NavigateTo(Queue<string> paths)
{
if (paths.TryDequeue(out string directory))
{
// Go to parent
if (directory == "..")
{
_path.TryPop(out _);
// Share changed
if (_path.Count == 0)
{
CurrentShare = null;
}
return NavigateTo(paths);
}
// Stay in same folder
if (directory == "." || directory == "")
{
return NavigateTo(paths);
}
// Go to child directory
if (GetDirectories().Select(x => Path.GetFileName(x)).Contains(directory))
{
// Share changed
if (_path.Count == 0)
{
CurrentShare = Shares.First(x => x.Name == directory);
}
_path.Push(directory);
return NavigateTo(paths);
}
else
{
return false;
}
}
return true;
}
private string GetRealPath()
{
if (_path.Count < 1)
{
return null;
}
return Path.Combine(_path.Skip(1).Prepend(CurrentShare.Path).ToArray());
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace DearFTP.Utils
{
public static class PasswordHash
{
private static readonly HashAlgorithm _hasher = SHA512.Create();
private const string _salt = "DearFTP";
public static string GetHash(string password)
{
var bytes = Encoding.UTF8.GetBytes($"{_salt}{password}");
return string.Join("", _hasher.ComputeHash(bytes).Select(x => x.ToString("X2"))).ToLower();
}
}
}