Initial commit.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -337,4 +337,5 @@ ASALocalRun/
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
healthchecksdb
|
||||
/.vscode
|
||||
|
||||
71
DearFTP/Configurations/Configuration.cs
Normal file
71
DearFTP/Configurations/Configuration.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
DearFTP/Configurations/ServerConfiguration.cs
Normal file
18
DearFTP/Configurations/ServerConfiguration.cs
Normal 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; } = "";
|
||||
}
|
||||
}
|
||||
28
DearFTP/Configurations/Share.cs
Normal file
28
DearFTP/Configurations/Share.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
DearFTP/Configurations/User.cs
Normal file
25
DearFTP/Configurations/User.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
DearFTP/Connection/Commands/ClntCommand.cs
Normal file
19
DearFTP/Connection/Commands/ClntCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
55
DearFTP/Connection/Commands/CommandsDispatcher.cs
Normal file
55
DearFTP/Connection/Commands/CommandsDispatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
DearFTP/Connection/Commands/CwdCommand.cs
Normal file
27
DearFTP/Connection/Commands/CwdCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
DearFTP/Connection/Commands/DeleteCommand.cs
Normal file
70
DearFTP/Connection/Commands/DeleteCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
DearFTP/Connection/Commands/FeaturesCommand.cs
Normal file
31
DearFTP/Connection/Commands/FeaturesCommand.cs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
DearFTP/Connection/Commands/FileModificationTimeCommand.cs
Normal file
70
DearFTP/Connection/Commands/FileModificationTimeCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
72
DearFTP/Connection/Commands/HelpCommand.cs
Normal file
72
DearFTP/Connection/Commands/HelpCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
DearFTP/Connection/Commands/ICommand.cs
Normal file
13
DearFTP/Connection/Commands/ICommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
141
DearFTP/Connection/Commands/ListCommand.cs
Normal file
141
DearFTP/Connection/Commands/ListCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
144
DearFTP/Connection/Commands/ListMachineCommand.cs
Normal file
144
DearFTP/Connection/Commands/ListMachineCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
DearFTP/Connection/Commands/MakeDirectoryCommand.cs
Normal file
56
DearFTP/Connection/Commands/MakeDirectoryCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
27
DearFTP/Connection/Commands/ParentDirectoryCommand.cs
Normal file
27
DearFTP/Connection/Commands/ParentDirectoryCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
DearFTP/Connection/Commands/PassiveCommand.cs
Normal file
30
DearFTP/Connection/Commands/PassiveCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
DearFTP/Connection/Commands/PwdCommand.cs
Normal file
20
DearFTP/Connection/Commands/PwdCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
DearFTP/Connection/Commands/QuitCommand.cs
Normal file
20
DearFTP/Connection/Commands/QuitCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
80
DearFTP/Connection/Commands/RenameCommand.cs
Normal file
80
DearFTP/Connection/Commands/RenameCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
71
DearFTP/Connection/Commands/RetrieveCommand.cs
Normal file
71
DearFTP/Connection/Commands/RetrieveCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
DearFTP/Connection/Commands/SiteCommand.cs
Normal file
19
DearFTP/Connection/Commands/SiteCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
37
DearFTP/Connection/Commands/SizeCommand.cs
Normal file
37
DearFTP/Connection/Commands/SizeCommand.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
79
DearFTP/Connection/Commands/StoreCommand.cs
Normal file
79
DearFTP/Connection/Commands/StoreCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
DearFTP/Connection/Commands/SystemCommand.cs
Normal file
19
DearFTP/Connection/Commands/SystemCommand.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
DearFTP/Connection/Commands/TypeCommand.cs
Normal file
20
DearFTP/Connection/Commands/TypeCommand.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
60
DearFTP/Connection/Commands/UserCommand.cs
Normal file
60
DearFTP/Connection/Commands/UserCommand.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
59
DearFTP/Connection/DataConnection.cs
Normal file
59
DearFTP/Connection/DataConnection.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
80
DearFTP/Connection/FtpStream.cs
Normal file
80
DearFTP/Connection/FtpStream.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
216
DearFTP/Connection/ResponseCode.cs
Normal file
216
DearFTP/Connection/ResponseCode.cs
Normal 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 server’s 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 wasn’t accepted and the requested action didn’t 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>
|
||||
/// Can’t 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
|
||||
}
|
||||
}
|
||||
91
DearFTP/Connection/Session.cs
Normal file
91
DearFTP/Connection/Session.cs
Normal 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
91
DearFTP/FtpServer.cs
Normal 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
56
DearFTP/Logger.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
275
DearFTP/Utils/NavigablePath.cs
Normal file
275
DearFTP/Utils/NavigablePath.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
21
DearFTP/Utils/PasswordHash.cs
Normal file
21
DearFTP/Utils/PasswordHash.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user