Initial commit.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -337,4 +337,5 @@ ASALocalRun/
|
|||||||
.localhistory/
|
.localhistory/
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
# 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
|
namespace DearFTP
|
||||||
{
|
{
|
||||||
@@ -6,7 +12,47 @@ namespace DearFTP
|
|||||||
{
|
{
|
||||||
static void Main(string[] args)
|
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