Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ClusterM/clovershell-client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2017-04-25 20:58:26 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2017-04-25 20:58:26 +0300
commit99a030b807969f3750b3f9b89f236476316f7ec8 (patch)
treec54e2a350474bdccee7e183260a1630a195f74c2
parentda2e29872d6d7aee0273315182fa33e2da2e568b (diff)
chown operations support for FTP server
-rw-r--r--CloverShell/ClovershellConnection.cs3
-rw-r--r--FtpServer/DebugLogHandler.cs148
-rw-r--r--FtpServer/IFileSystemHandler.cs407
-rw-r--r--FtpServer/NesMiniAuthHandler.cs6
-rw-r--r--FtpServer/NesMiniFileSystemHandler.cs642
-rw-r--r--FtpServer/Server.cs2
-rw-r--r--FtpServer/Session.cs2111
7 files changed, 1679 insertions, 1640 deletions
diff --git a/CloverShell/ClovershellConnection.cs b/CloverShell/ClovershellConnection.cs
index ae247c9..79d5170 100644
--- a/CloverShell/ClovershellConnection.cs
+++ b/CloverShell/ClovershellConnection.cs
@@ -422,7 +422,8 @@ namespace com.clusterrr.clovershell
len -= tLen;
if (res != ErrorCode.Ok)
{
- if (repeats >= 3) break;
+ if (repeats >= 10) break;
+ Debug.WriteLine("clovershell write error: " + res.ToString());
repeats++;
Thread.Sleep(100);
}
diff --git a/FtpServer/DebugLogHandler.cs b/FtpServer/DebugLogHandler.cs
index b975f99..292d5ee 100644
--- a/FtpServer/DebugLogHandler.cs
+++ b/FtpServer/DebugLogHandler.cs
@@ -1,74 +1,74 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-
-namespace mooftpserv
-{
- /// <summary>
- /// Default log handler.
- ///
- public class DebugLogHandler : ILogHandler
- {
- private IPEndPoint peer;
-
- public DebugLogHandler()
- {
- }
-
- private DebugLogHandler(IPEndPoint peer)
- {
- this.peer = peer;
- }
-
- public ILogHandler Clone(IPEndPoint peer)
- {
- return new DebugLogHandler(peer);
- }
-
- private void Write(string format, params object[] args)
- {
- Debug.WriteLine(String.Format("{0}: {1}", peer, String.Format(format, args)));
- }
-
- public void NewControlConnection()
- {
- Write("new control connection");
- }
-
- public void ClosedControlConnection()
- {
- Write("closed control connection");
- }
-
- public void ReceivedCommand(string verb, string arguments)
- {
-#if VERY_DEBUG
- string argtext = (arguments == null || arguments == "" ? "" : ' ' + arguments);
- Write("received command: {0}{1}", verb, argtext);
-#endif
- }
-
- public void SentResponse(uint code, string description)
- {
-#if VERY_DEBUG
- Write("sent response: {0} {1}", code, description);
-#endif
- }
-
- public void NewDataConnection(IPEndPoint remote, IPEndPoint local, bool passive)
- {
-#if VERY_DEBUG
- Write("new data connection: {0} <-> {1} ({2})", remote, local, (passive ? "passive" : "active"));
-#endif
- }
-
- public void ClosedDataConnection(IPEndPoint remote, IPEndPoint local, bool passive)
- {
-#if VERY_DEBUG
- Write("closed data connection: {0} <-> {1} ({2})", remote, local, (passive ? "passive" : "active"));
-#endif
- }
- }
-}
-
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// Default log handler.
+ ///
+ public class DebugLogHandler : ILogHandler
+ {
+ private IPEndPoint peer;
+
+ public DebugLogHandler()
+ {
+ }
+
+ private DebugLogHandler(IPEndPoint peer)
+ {
+ this.peer = peer;
+ }
+
+ public ILogHandler Clone(IPEndPoint peer)
+ {
+ return new DebugLogHandler(peer);
+ }
+
+ private void Write(string format, params object[] args)
+ {
+ Debug.WriteLine(String.Format("{0}: {1}", peer, String.Format(format, args)));
+ }
+
+ public void NewControlConnection()
+ {
+ Write("new control connection");
+ }
+
+ public void ClosedControlConnection()
+ {
+ Write("closed control connection");
+ }
+
+ public void ReceivedCommand(string verb, string arguments)
+ {
+#if VERY_DEBUG
+ string argtext = (arguments == null || arguments == "" ? "" : ' ' + arguments);
+ Write("received command: {0}{1}", verb, argtext);
+#endif
+ }
+
+ public void SentResponse(uint code, string description)
+ {
+#if VERY_DEBUG
+ Write("sent response: {0} {1}", code, description);
+#endif
+ }
+
+ public void NewDataConnection(IPEndPoint remote, IPEndPoint local, bool passive)
+ {
+#if VERY_DEBUG
+ Write("new data connection: {0} <-> {1} ({2})", remote, local, (passive ? "passive" : "active"));
+#endif
+ }
+
+ public void ClosedDataConnection(IPEndPoint remote, IPEndPoint local, bool passive)
+ {
+#if VERY_DEBUG
+ Write("closed data connection: {0} <-> {1} ({2})", remote, local, (passive ? "passive" : "active"));
+#endif
+ }
+ }
+}
+
diff --git a/FtpServer/IFileSystemHandler.cs b/FtpServer/IFileSystemHandler.cs
index bf4eb70..e54adb5 100644
--- a/FtpServer/IFileSystemHandler.cs
+++ b/FtpServer/IFileSystemHandler.cs
@@ -1,202 +1,205 @@
-using System;
-using System.Net;
-using System.IO;
-
-namespace mooftpserv
-{
- /// <summary>
- /// File system entry as returned by List.
- /// </summary>
- public struct FileSystemEntry
- {
- public string Name;
- public bool IsDirectory;
- public long Size;
- public DateTime LastModifiedTimeUtc;
- }
-
- /// <summary>
- /// Wrapper that either contains a value or an error string.
- /// </summary>
- public class ResultOrError<T>
- {
- private T result;
- private string error;
-
- private ResultOrError(T result, string error)
- {
- this.result = result;
- this.error = error;
- }
-
- public static ResultOrError<T> MakeResult(T result)
- {
- return new ResultOrError<T>(result, null);
- }
-
- public static ResultOrError<T> MakeError(string error)
- {
- if (error == null)
- throw new ArgumentNullException();
- return new ResultOrError<T>(default(T), error.Replace(Environment.NewLine, " "));
- }
-
- public bool HasError
- {
- get { return error != null; }
- }
-
- public string Error
- {
- get { return error; }
- }
-
- public T Result
- {
- get
- {
- if (HasError)
- throw new InvalidOperationException(String.Format("No result available, error: {0}", error));
- return result;
- }
- }
- };
-
- /// <summary>
- /// Interface for file system access from FTP.
- /// </summary>
- public interface IFileSystemHandler
- {
- /// <summary>
- /// Make a new instance for a new session with the given peer.
- /// Each FTP session uses a separate, cloned instance.
- /// </summary>
- IFileSystemHandler Clone(IPEndPoint peer);
-
- /// <summary>
- /// PWD: Returns the path of the current working directory.
- /// </summary>
- /// <returns>
- /// The absolute path of the current directory or an error string.
- /// </returns>
- ResultOrError<string> GetCurrentDirectory();
-
- /// <summary>
- /// CWD: Changes the current directory.
- /// CDUP: Changes to parent directory (called with "..")
- /// </summary>
- /// <returns>
- /// The new absolute path or an error string.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path to which to change.
- /// </param>
- ResultOrError<string> ChangeDirectory(string path);
-
- /// <summary>
- /// MKD: Create a directory.
- /// </summary>
- /// <returns>
- /// The absolute path of the created directory or an error string.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path for the new directory.
- /// </param>
- ResultOrError<string> CreateDirectory(string path);
-
- /// <summary>
- /// RMD: Remove a directory.
- /// </summary>
- /// <returns>
- /// A bool or an error string. The bool is not actually used.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path for the directory.
- /// </param>
- ResultOrError<bool> RemoveDirectory(string path);
-
- /// <summary>
- /// RETR: Open a stream for reading the specified file.
- /// </summary>
- /// <returns>
- /// An opened stream for reading from the file, or an error string.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path for the file.
- /// </param>
- ResultOrError<Stream> ReadFile(string path);
-
- /// <summary>
- /// STOR: Open a stream for writing to the specified file.
- /// If the file exists, it should be overwritten.
- /// </summary>
- /// <returns>
- /// An opened stream for writing to the file, or an error string.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path for the file.
- /// </param>
- ResultOrError<Stream> WriteFile(string path);
- ResultOrError<bool> WriteFileFinalize(string path, Stream stream);
-
- /// <summary>
- /// DELE: Deletes a file.
- /// </summary>
- /// <returns>
- /// A bool or an error string. The bool is not actually used.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path for the file.
- /// </param>
- ResultOrError<bool> RemoveFile(string path);
-
- /// <summary>
- /// RNFR, RNTO: Renames or moves a file or directory.
- /// </summary>
- /// <returns>
- /// A bool or an error string. The bool is not actually used.
- /// </returns>
- /// <param name="fromPath">
- /// The relative or absolute path of an existing file or directory.
- /// </param>
- /// <param name="toPath">
- /// A relative or absolute non-existing path to which the file will be renamed or moved.
- /// </param>
- ResultOrError<bool> RenameFile(string fromPath, string toPath);
-
- /// <summary>
- /// LIST: Return a list of files and folders in a directory, or for a file (like 'ls').
- /// </summary>
- /// <param name="path">
- /// The relative or absolute path of an existing directory or file.
- /// Can be null or empty to return the current directory.
- /// </para>
- /// <return>
- /// An array of file system entries or an error string.
- /// </return>
- ResultOrError<FileSystemEntry[]> ListEntries(string path);
-
- /// <summary>
- /// SIZE: Gets the size of a file in bytes.
- /// </summary>
- /// <returns>
- /// The file size, or -1 on error.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path.
- /// </param>
- ResultOrError<long> GetFileSize(string path);
-
- /// <summary>
- /// MDTM: Gets the last modified timestamp of a file.
- /// </summary>
- /// <returns>
- /// The last modified time in UTC, or an error string.
- /// </returns>
- /// <param name='path'>
- /// A relative or absolute path.
- /// </param>
- ResultOrError<DateTime> GetLastModifiedTimeUtc(string path);
- }
-}
-
+using System;
+using System.Net;
+using System.IO;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// File system entry as returned by List.
+ /// </summary>
+ public struct FileSystemEntry
+ {
+ public string Name;
+ public bool IsDirectory;
+ public long Size;
+ public DateTime LastModifiedTimeUtc;
+ public string Mode;
+ }
+
+ /// <summary>
+ /// Wrapper that either contains a value or an error string.
+ /// </summary>
+ public class ResultOrError<T>
+ {
+ private T result;
+ private string error;
+
+ private ResultOrError(T result, string error)
+ {
+ this.result = result;
+ this.error = error;
+ }
+
+ public static ResultOrError<T> MakeResult(T result)
+ {
+ return new ResultOrError<T>(result, null);
+ }
+
+ public static ResultOrError<T> MakeError(string error)
+ {
+ if (error == null)
+ throw new ArgumentNullException();
+ return new ResultOrError<T>(default(T), error.Replace(Environment.NewLine, " "));
+ }
+
+ public bool HasError
+ {
+ get { return error != null; }
+ }
+
+ public string Error
+ {
+ get { return error; }
+ }
+
+ public T Result
+ {
+ get
+ {
+ if (HasError)
+ throw new InvalidOperationException(String.Format("No result available, error: {0}", error));
+ return result;
+ }
+ }
+ };
+
+ /// <summary>
+ /// Interface for file system access from FTP.
+ /// </summary>
+ public interface IFileSystemHandler
+ {
+ /// <summary>
+ /// Make a new instance for a new session with the given peer.
+ /// Each FTP session uses a separate, cloned instance.
+ /// </summary>
+ IFileSystemHandler Clone(IPEndPoint peer);
+
+ /// <summary>
+ /// PWD: Returns the path of the current working directory.
+ /// </summary>
+ /// <returns>
+ /// The absolute path of the current directory or an error string.
+ /// </returns>
+ ResultOrError<string> GetCurrentDirectory();
+
+ /// <summary>
+ /// CWD: Changes the current directory.
+ /// CDUP: Changes to parent directory (called with "..")
+ /// </summary>
+ /// <returns>
+ /// The new absolute path or an error string.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path to which to change.
+ /// </param>
+ ResultOrError<string> ChangeDirectory(string path);
+
+ /// <summary>
+ /// MKD: Create a directory.
+ /// </summary>
+ /// <returns>
+ /// The absolute path of the created directory or an error string.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path for the new directory.
+ /// </param>
+ ResultOrError<string> CreateDirectory(string path);
+
+ /// <summary>
+ /// RMD: Remove a directory.
+ /// </summary>
+ /// <returns>
+ /// A bool or an error string. The bool is not actually used.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path for the directory.
+ /// </param>
+ ResultOrError<bool> RemoveDirectory(string path);
+
+ /// <summary>
+ /// RETR: Open a stream for reading the specified file.
+ /// </summary>
+ /// <returns>
+ /// An opened stream for reading from the file, or an error string.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path for the file.
+ /// </param>
+ ResultOrError<Stream> ReadFile(string path);
+
+ /// <summary>
+ /// STOR: Open a stream for writing to the specified file.
+ /// If the file exists, it should be overwritten.
+ /// </summary>
+ /// <returns>
+ /// An opened stream for writing to the file, or an error string.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path for the file.
+ /// </param>
+ ResultOrError<Stream> WriteFile(string path);
+ ResultOrError<bool> WriteFileFinalize(string path, Stream stream);
+
+ /// <summary>
+ /// DELE: Deletes a file.
+ /// </summary>
+ /// <returns>
+ /// A bool or an error string. The bool is not actually used.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path for the file.
+ /// </param>
+ ResultOrError<bool> RemoveFile(string path);
+
+ /// <summary>
+ /// RNFR, RNTO: Renames or moves a file or directory.
+ /// </summary>
+ /// <returns>
+ /// A bool or an error string. The bool is not actually used.
+ /// </returns>
+ /// <param name="fromPath">
+ /// The relative or absolute path of an existing file or directory.
+ /// </param>
+ /// <param name="toPath">
+ /// A relative or absolute non-existing path to which the file will be renamed or moved.
+ /// </param>
+ ResultOrError<bool> RenameFile(string fromPath, string toPath);
+
+ /// <summary>
+ /// LIST: Return a list of files and folders in a directory, or for a file (like 'ls').
+ /// </summary>
+ /// <param name="path">
+ /// The relative or absolute path of an existing directory or file.
+ /// Can be null or empty to return the current directory.
+ /// </para>
+ /// <return>
+ /// An array of file system entries or an error string.
+ /// </return>
+ ResultOrError<FileSystemEntry[]> ListEntries(string path);
+
+ /// <summary>
+ /// SIZE: Gets the size of a file in bytes.
+ /// </summary>
+ /// <returns>
+ /// The file size, or -1 on error.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path.
+ /// </param>
+ ResultOrError<long> GetFileSize(string path);
+
+ /// <summary>
+ /// MDTM: Gets the last modified timestamp of a file.
+ /// </summary>
+ /// <returns>
+ /// The last modified time in UTC, or an error string.
+ /// </returns>
+ /// <param name='path'>
+ /// A relative or absolute path.
+ /// </param>
+ ResultOrError<DateTime> GetLastModifiedTimeUtc(string path);
+
+ ResultOrError<bool> ChmodFile(string mode, string path);
+ }
+}
+
diff --git a/FtpServer/NesMiniAuthHandler.cs b/FtpServer/NesMiniAuthHandler.cs
index 66ea072..3da4c2b 100644
--- a/FtpServer/NesMiniAuthHandler.cs
+++ b/FtpServer/NesMiniAuthHandler.cs
@@ -9,15 +9,15 @@ namespace mooftpserv
public NesMiniAuthHandler()
{
- }
-
+ }
+
private NesMiniAuthHandler(IPEndPoint peer)
{
this.peer = peer;
}
public IAuthHandler Clone(IPEndPoint peer)
- {
+ {
return new NesMiniAuthHandler(peer);
}
diff --git a/FtpServer/NesMiniFileSystemHandler.cs b/FtpServer/NesMiniFileSystemHandler.cs
index 86c8e01..f8a768b 100644
--- a/FtpServer/NesMiniFileSystemHandler.cs
+++ b/FtpServer/NesMiniFileSystemHandler.cs
@@ -1,314 +1,328 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.IO;
-using com.clusterrr.clovershell;
-
-namespace mooftpserv
-{
- public class NesMiniFileSystemHandler : IFileSystemHandler
- {
- // list of supported operating systems
- private enum OS { WinNT, WinCE, Unix };
-
- // currently used operating system
- private OS os;
- // current path as TVFS or unix-like
- private string currentPath;
- // clovershell
- private ClovershellConnection clovershell;
-
- public NesMiniFileSystemHandler(ClovershellConnection clovershell, string startPath)
- {
- os = OS.Unix;
- this.currentPath = startPath;
- this.clovershell = clovershell;
- }
-
- public NesMiniFileSystemHandler(ClovershellConnection clovershell)
- : this(clovershell, "/")
- {
- }
-
- private NesMiniFileSystemHandler(string path, OS os, ClovershellConnection clovershell)
- {
- this.currentPath = path;
- this.os = os;
- this.clovershell = clovershell;
- }
-
- public IFileSystemHandler Clone(IPEndPoint peer)
- {
- return new NesMiniFileSystemHandler(currentPath, os, clovershell);
- }
-
- public ResultOrError<string> GetCurrentDirectory()
- {
- return MakeResult<string>(currentPath);
- }
-
- public ResultOrError<string> ChangeDirectory(string path)
- {
- string newPath = ResolvePath(path);
- currentPath = newPath;
- return MakeResult<string>(newPath);
- }
-
- public ResultOrError<string> ChangeToParentDirectory()
- {
- return ChangeDirectory("..");
- }
-
- public ResultOrError<string> CreateDirectory(string path)
- {
- string newPath = ResolvePath(path);
- try
- {
- foreach (var c in newPath)
- if ((int)c > 255) throw new Exception("Invalid characters in directory name");
- var newpath = DecodePath(newPath);
- clovershell.ExecuteSimple("mkdir \"" + newpath + "\"");
- }
- catch (Exception ex)
- {
- return MakeError<string>(ex.Message);
- }
-
- return MakeResult<string>(newPath);
- }
-
- public ResultOrError<bool> RemoveDirectory(string path)
- {
- string newPath = ResolvePath(path);
-
- try
- {
- var rpath = DecodePath(newPath);
- clovershell.ExecuteSimple("rm -rf \"" + rpath + "\"");
- }
- catch (Exception ex)
- {
- return MakeError<bool>(ex.Message);
- }
-
- return MakeResult<bool>(true);
- }
-
- public ResultOrError<Stream> ReadFile(string path)
- {
- string newPath = ResolvePath(path);
- try
- {
- var data = new MemoryStream();
- clovershell.Execute("cat \"" + newPath + "\"", null, data, null, 1000, true);
- data.Seek(0, SeekOrigin.Begin);
- return MakeResult<Stream>(data);
- }
- catch (Exception ex)
- {
- return MakeError<Stream>(ex.Message);
- }
- }
-
- public ResultOrError<Stream> WriteFile(string path)
- {
- string newPath = ResolvePath(path);
- try
- {
- foreach (var c in newPath)
- if ((int)c > 255) throw new Exception("Invalid characters in directory name");
- return MakeResult<Stream>(new MemoryStream());
- }
- catch (Exception ex)
- {
- return MakeError<Stream>(ex.Message);
- }
- }
-
- public ResultOrError<bool> WriteFileFinalize(string path, Stream str)
- {
- string newPath = ResolvePath(path);
- try
- {
- str.Seek(0, SeekOrigin.Begin);
- clovershell.Execute("cat > \"" + newPath + "\"", str, null, null, 1000, true);
- str.Dispose();
- return MakeResult<bool>(true);
- }
- catch (Exception ex)
- {
- return MakeError<bool>(ex.Message);
- }
- }
-
- public ResultOrError<bool> RemoveFile(string path)
- {
- string newPath = ResolvePath(path);
-
- try
- {
- clovershell.ExecuteSimple("rm -rf \"" + newPath + "\"", 1000, true);
- }
- catch (Exception ex)
- {
- return MakeError<bool>(ex.Message);
- }
-
- return MakeResult<bool>(true);
- }
-
- public ResultOrError<bool> RenameFile(string fromPath, string toPath)
- {
- fromPath = ResolvePath(fromPath);
- toPath = ResolvePath(toPath);
- try
- {
- clovershell.ExecuteSimple("mv \"" + fromPath + "\" \"" + toPath + "\"", 1000, true);
- }
- catch (Exception ex)
- {
- return MakeError<bool>(ex.Message);
- }
-
- return MakeResult<bool>(true);
- }
-
- public ResultOrError<FileSystemEntry[]> ListEntries(string path)
- {
- string newPath = ResolvePath(path);
- List<FileSystemEntry> result = new List<FileSystemEntry>();
- try
- {
- var lines = clovershell.ExecuteSimple("ls -lep \"" + newPath + "\"", 1000, true)
- .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
- {
- if (line.StartsWith("total")) continue;
- FileSystemEntry entry = new FileSystemEntry();
- entry.Name = line.Substring(69).Trim();
- entry.IsDirectory = entry.Name.EndsWith("/");
- if (entry.IsDirectory) entry.Name = entry.Name.Substring(0, entry.Name.Length - 1);
- entry.Size = long.Parse(line.Substring(29, 15).Trim());
- // Who cares? There is no time source on NES Mini
- //DateTime.Parse(line.Substring(44, 25).Trim());
- entry.LastModifiedTimeUtc = DateTime.MinValue;
- result.Add(entry);
- }
- }
- catch (Exception ex)
- {
- return MakeError<FileSystemEntry[]>(ex.Message);
- }
-
- return MakeResult<FileSystemEntry[]>(result.ToArray());
- }
-
- public ResultOrError<long> GetFileSize(string path)
- {
- string newPath = ResolvePath(path);
- List<FileSystemEntry> result = new List<FileSystemEntry>();
- try
- {
- var lines = clovershell.ExecuteSimple("ls -le \"" + newPath + "\"", 1000, true)
- .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
- {
- return MakeResult<long>(long.Parse(line.Substring(29, 15).Trim()));
- }
- }
- catch (Exception ex)
- {
- return MakeError<long>(ex.Message);
- }
- return MakeResult<long>(0);
- }
-
- public ResultOrError<DateTime> GetLastModifiedTimeUtc(string path)
- {
- /*
- string newPath = ResolvePath(path);
- List<FileSystemEntry> result = new List<FileSystemEntry>();
- try
- {
- var lines = clovershell.ExecuteSimple("ls -le \"" + newPath + "\"", 1000, true)
- .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
- {
- MakeResult<DateTime>(DateTime.Parse(line.Substring(45, 25).Trim()));
- }
- }
- catch (Exception ex)
- {
- return MakeError<DateTime>(ex.Message);
- }
- */
- return MakeResult<DateTime>(DateTime.MinValue);
- }
-
- private string ResolvePath(string path)
- {
- if (path == null) return currentPath;
- if (path.Contains(" -> "))
- path = path.Substring(path.IndexOf(" -> ") + 4);
- return FileSystemHelper.ResolvePath(currentPath, path);
- }
-
- private string EncodePath(string path)
- {
- if (os == OS.WinNT)
- return "/" + path[0] + (path.Length > 2 ? path.Substring(2).Replace(@"\", "/") : "");
- else if (os == OS.WinCE)
- return path.Replace(@"\", "/");
- else
- return path;
- }
-
- private string DecodePath(string path)
- {
- if (path == null || path == "" || path[0] != '/')
- return null;
-
- if (os == OS.WinNT)
- {
- // some error checking for the drive layer
- if (path == "/")
- return null; // should have been caught elsewhere
-
- if (path.Length > 1 && path[1] == '/')
- return null;
-
- if (path.Length > 2 && path[2] != '/')
- return null;
-
- if (path.Length < 4) // e.g. "/C/"
- return path[1] + @":\";
- else
- return path[1] + @":\" + path.Substring(3).Replace("/", @"\");
- }
- else if (os == OS.WinCE)
- {
- return path.Replace("/", @"\");
- }
- else
- {
- return path;
- }
- }
-
- /// <summary>
- /// Shortcut for ResultOrError<T>.MakeResult()
- /// </summary>
- private ResultOrError<T> MakeResult<T>(T result)
- {
- return ResultOrError<T>.MakeResult(result);
- }
-
- /// <summary>
- /// Shortcut for ResultOrError<T>.MakeError()
- /// </summary>
- private ResultOrError<T> MakeError<T>(string error)
- {
- return ResultOrError<T>.MakeError(error);
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.IO;
+using com.clusterrr.clovershell;
+
+namespace mooftpserv
+{
+ public class NesMiniFileSystemHandler : IFileSystemHandler
+ {
+ // list of supported operating systems
+ private enum OS { WinNT, WinCE, Unix };
+
+ // currently used operating system
+ private OS os;
+ // current path as TVFS or unix-like
+ private string currentPath;
+ // clovershell
+ private ClovershellConnection clovershell;
+
+ public NesMiniFileSystemHandler(ClovershellConnection clovershell, string startPath)
+ {
+ os = OS.Unix;
+ this.currentPath = startPath;
+ this.clovershell = clovershell;
+ }
+
+ public NesMiniFileSystemHandler(ClovershellConnection clovershell)
+ : this(clovershell, "/")
+ {
+ }
+
+ private NesMiniFileSystemHandler(string path, OS os, ClovershellConnection clovershell)
+ {
+ this.currentPath = path;
+ this.os = os;
+ this.clovershell = clovershell;
+ }
+
+ public IFileSystemHandler Clone(IPEndPoint peer)
+ {
+ return new NesMiniFileSystemHandler(currentPath, os, clovershell);
+ }
+
+ public ResultOrError<string> GetCurrentDirectory()
+ {
+ return MakeResult<string>(currentPath);
+ }
+
+ public ResultOrError<string> ChangeDirectory(string path)
+ {
+ string newPath = ResolvePath(path);
+ currentPath = newPath;
+ return MakeResult<string>(newPath);
+ }
+
+ public ResultOrError<string> ChangeToParentDirectory()
+ {
+ return ChangeDirectory("..");
+ }
+
+ public ResultOrError<string> CreateDirectory(string path)
+ {
+ string newPath = ResolvePath(path);
+ try
+ {
+ foreach (var c in newPath)
+ if ((int)c > 255) throw new Exception("Invalid characters in directory name");
+ var newpath = DecodePath(newPath);
+ clovershell.ExecuteSimple("mkdir \"" + newpath + "\"");
+ }
+ catch (Exception ex)
+ {
+ return MakeError<string>(ex.Message);
+ }
+
+ return MakeResult<string>(newPath);
+ }
+
+ public ResultOrError<bool> RemoveDirectory(string path)
+ {
+ string newPath = ResolvePath(path);
+
+ try
+ {
+ var rpath = DecodePath(newPath);
+ clovershell.ExecuteSimple("rm -rf \"" + rpath + "\"");
+ }
+ catch (Exception ex)
+ {
+ return MakeError<bool>(ex.Message);
+ }
+
+ return MakeResult<bool>(true);
+ }
+
+ public ResultOrError<Stream> ReadFile(string path)
+ {
+ string newPath = ResolvePath(path);
+ try
+ {
+ var data = new MemoryStream();
+ clovershell.Execute("cat \"" + newPath + "\"", null, data, null, 1000, true);
+ data.Seek(0, SeekOrigin.Begin);
+ return MakeResult<Stream>(data);
+ }
+ catch (Exception ex)
+ {
+ return MakeError<Stream>(ex.Message);
+ }
+ }
+
+ public ResultOrError<Stream> WriteFile(string path)
+ {
+ string newPath = ResolvePath(path);
+ try
+ {
+ foreach (var c in newPath)
+ if ((int)c > 255) throw new Exception("Invalid characters in directory name");
+ return MakeResult<Stream>(new MemoryStream());
+ }
+ catch (Exception ex)
+ {
+ return MakeError<Stream>(ex.Message);
+ }
+ }
+
+ public ResultOrError<bool> WriteFileFinalize(string path, Stream str)
+ {
+ string newPath = ResolvePath(path);
+ try
+ {
+ str.Seek(0, SeekOrigin.Begin);
+ clovershell.Execute("cat > \"" + newPath + "\"", str, null, null, 1000, true);
+ str.Dispose();
+ return MakeResult<bool>(true);
+ }
+ catch (Exception ex)
+ {
+ return MakeError<bool>(ex.Message);
+ }
+ }
+
+ public ResultOrError<bool> RemoveFile(string path)
+ {
+ string newPath = ResolvePath(path);
+
+ try
+ {
+ clovershell.ExecuteSimple("rm -rf \"" + newPath + "\"", 1000, true);
+ }
+ catch (Exception ex)
+ {
+ return MakeError<bool>(ex.Message);
+ }
+
+ return MakeResult<bool>(true);
+ }
+
+ public ResultOrError<bool> RenameFile(string fromPath, string toPath)
+ {
+ fromPath = ResolvePath(fromPath);
+ toPath = ResolvePath(toPath);
+ try
+ {
+ clovershell.ExecuteSimple("mv \"" + fromPath + "\" \"" + toPath + "\"", 1000, true);
+ }
+ catch (Exception ex)
+ {
+ return MakeError<bool>(ex.Message);
+ }
+
+ return MakeResult<bool>(true);
+ }
+
+ public ResultOrError<FileSystemEntry[]> ListEntries(string path)
+ {
+ string newPath = ResolvePath(path);
+ List<FileSystemEntry> result = new List<FileSystemEntry>();
+ try
+ {
+ var lines = clovershell.ExecuteSimple("ls -lep \"" + newPath + "\"", 1000, true)
+ .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ if (line.StartsWith("total")) continue;
+ FileSystemEntry entry = new FileSystemEntry();
+ entry.Mode = line.Substring(1, 12).Trim();
+ entry.Name = line.Substring(69).Trim();
+ entry.IsDirectory = entry.Name.EndsWith("/");
+ if (entry.IsDirectory) entry.Name = entry.Name.Substring(0, entry.Name.Length - 1);
+ entry.Size = long.Parse(line.Substring(29, 15).Trim());
+ // Who cares? There is no time source on NES Mini
+ //DateTime.Parse(line.Substring(44, 25).Trim());
+ entry.LastModifiedTimeUtc = DateTime.MinValue;
+ result.Add(entry);
+ }
+ }
+ catch (Exception ex)
+ {
+ return MakeError<FileSystemEntry[]>(ex.Message);
+ }
+
+ return MakeResult<FileSystemEntry[]>(result.ToArray());
+ }
+
+ public ResultOrError<long> GetFileSize(string path)
+ {
+ string newPath = ResolvePath(path);
+ List<FileSystemEntry> result = new List<FileSystemEntry>();
+ try
+ {
+ var lines = clovershell.ExecuteSimple("ls -le \"" + newPath + "\"", 1000, true)
+ .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ return MakeResult<long>(long.Parse(line.Substring(29, 15).Trim()));
+ }
+ }
+ catch (Exception ex)
+ {
+ return MakeError<long>(ex.Message);
+ }
+ return MakeResult<long>(0);
+ }
+
+ public ResultOrError<DateTime> GetLastModifiedTimeUtc(string path)
+ {
+ /*
+ string newPath = ResolvePath(path);
+ List<FileSystemEntry> result = new List<FileSystemEntry>();
+ try
+ {
+ var lines = clovershell.ExecuteSimple("ls -le \"" + newPath + "\"", 1000, true)
+ .Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ MakeResult<DateTime>(DateTime.Parse(line.Substring(45, 25).Trim()));
+ }
+ }
+ catch (Exception ex)
+ {
+ return MakeError<DateTime>(ex.Message);
+ }
+ */
+ return MakeResult<DateTime>(DateTime.MinValue);
+ }
+
+ private string ResolvePath(string path)
+ {
+ if (path == null) return currentPath;
+ if (path.Contains(" -> "))
+ path = path.Substring(path.IndexOf(" -> ") + 4);
+ return FileSystemHelper.ResolvePath(currentPath, path);
+ }
+
+ private string EncodePath(string path)
+ {
+ if (os == OS.WinNT)
+ return "/" + path[0] + (path.Length > 2 ? path.Substring(2).Replace(@"\", "/") : "");
+ else if (os == OS.WinCE)
+ return path.Replace(@"\", "/");
+ else
+ return path;
+ }
+
+ private string DecodePath(string path)
+ {
+ if (path == null || path == "" || path[0] != '/')
+ return null;
+
+ if (os == OS.WinNT)
+ {
+ // some error checking for the drive layer
+ if (path == "/")
+ return null; // should have been caught elsewhere
+
+ if (path.Length > 1 && path[1] == '/')
+ return null;
+
+ if (path.Length > 2 && path[2] != '/')
+ return null;
+
+ if (path.Length < 4) // e.g. "/C/"
+ return path[1] + @":\";
+ else
+ return path[1] + @":\" + path.Substring(3).Replace("/", @"\");
+ }
+ else if (os == OS.WinCE)
+ {
+ return path.Replace("/", @"\");
+ }
+ else
+ {
+ return path;
+ }
+ }
+
+ /// <summary>
+ /// Shortcut for ResultOrError<T>.MakeResult()
+ /// </summary>
+ private ResultOrError<T> MakeResult<T>(T result)
+ {
+ return ResultOrError<T>.MakeResult(result);
+ }
+
+ /// <summary>
+ /// Shortcut for ResultOrError<T>.MakeError()
+ /// </summary>
+ private ResultOrError<T> MakeError<T>(string error)
+ {
+ return ResultOrError<T>.MakeError(error);
+ }
+
+ public ResultOrError<bool> ChmodFile(string mode, string path)
+ {
+ try
+ {
+ clovershell.ExecuteSimple(string.Format("chmod {0} {1}", mode, path), 1000, true);
+ return ResultOrError<bool>.MakeResult(true);
+ }
+ catch (Exception ex)
+ {
+ return MakeError<bool>(ex.Message);
+ }
+ }
+ }
+}
diff --git a/FtpServer/Server.cs b/FtpServer/Server.cs
index 4a98e95..b544f8e 100644
--- a/FtpServer/Server.cs
+++ b/FtpServer/Server.cs
@@ -159,7 +159,7 @@ namespace mooftpserv
/// Stop the server.
/// </summary>
public void Stop()
- {
+ {
if (socket == null) return;
socket.Stop();
}
diff --git a/FtpServer/Session.cs b/FtpServer/Session.cs
index 312a077..c851662 100644
--- a/FtpServer/Session.cs
+++ b/FtpServer/Session.cs
@@ -1,1045 +1,1066 @@
-using System;
-using System.IO;
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-using System.Threading;
-
-namespace mooftpserv
-{
- /// <summary>
- /// FTP session/connection. Does all the heavy lifting of the FTP protocol.
- /// Reads commands, sends replies, manages data connections, and so on.
- /// Each session creates its own thread.
- /// </summary>
- class Session
- {
- // transfer data type, ascii or binary
- enum DataType { ASCII, IMAGE };
-
- // buffer size to use for reading commands from the control connection
- private static int CMD_BUFFER_SIZE = 4096;
- // version from AssemblyInfo
- private static string LIB_VERSION = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(2);
- // monthnames for LIST command, since DateTime returns localized names
- private static string[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
- // response text for initial response. preceeded by application name and version number.
- private static string[] HELLO_TEXT = { "What can I do for you?", "Good day, sir or madam.", "Hey ho let's go!", "The poor man's FTP server." };
- // response text for general ok messages
- private static string[] OK_TEXT = { "Sounds good.", "Success!", "Alright, I'll do it...", "Consider it done." };
- // Result for FEAT command
- private static string[] FEATURES = { "MDTM", "PASV", "SIZE", "TVFS", "UTF8" };
-
- // local EOL flavor
- private static byte[] localEolBytes = Encoding.ASCII.GetBytes(Environment.NewLine);
- // FTP-mandated EOL flavor (= CRLF)
- private static byte[] remoteEolBytes = Encoding.ASCII.GetBytes("\r\n");
- // on Windows, no ASCII conversion is necessary (CRLF == CRLF)
- private static bool noAsciiConv = (localEolBytes == remoteEolBytes);
-
- // socket for the control connection
- private Socket controlSocket;
- // buffer size to use for sending/receiving with data connections
- private int dataBufferSize;
- // auth handler, checks user credentials
- private IAuthHandler authHandler;
- // file system handler, implements file system access for the FTP commands
- private IFileSystemHandler fsHandler;
- // log handler, used for diagnostic logging output. can be null.
- private ILogHandler logHandler;
- // Session thread, the control and data connections are processed in this thread
- private Thread thread;
-
- // .NET CF does not have Thread.IsAlive, so this flag replaces it
- private bool threadAlive = false;
- // Random Number Generator for OK and HELLO texts
- private Random randomTextIndex;
- // flag for whether the user has successfully logged in
- private bool loggedIn = false;
- // name of the logged in user, also used to remember the username when waiting for the PASS command
- private string loggedInUser = null;
- // argument of pending RNFR command, when waiting for an RNTO command
- private string renameFromPath = null;
-
- // remote data port. null when PASV is used.
- private IPEndPoint dataPort = null;
- // socket for data connections
- private Socket dataSocket = null;
- // .NET CF does not have Socket.Bound, so this flag replaces it
- private bool dataSocketBound = false;
- // buffer for reading from the control connection
- private byte[] cmdRcvBuffer;
- // number of bytes in the cmdRcvBuffer
- private int cmdRcvBytes;
- // buffer for sending/receiving with data connections
- private byte[] dataBuffer;
- // data type of the session, can be changed by the client
- private DataType transferDataType = DataType.ASCII;
-
- /// <summary>
- /// Creates a new session, which can afterwards be started with Start().
- /// </summary>
- public Session(Socket socket, int bufferSize, IAuthHandler authHandler, IFileSystemHandler fileSystemHandler, ILogHandler logHandler)
- {
- this.controlSocket = socket;
- this.dataBufferSize = bufferSize;
- this.authHandler = authHandler;
- this.fsHandler = fileSystemHandler;
- this.logHandler = logHandler;
-
- this.cmdRcvBuffer = new byte[CMD_BUFFER_SIZE];
- this.cmdRcvBytes = 0;
- this.dataBuffer = new byte[dataBufferSize + 1]; // +1 for partial EOL
- this.randomTextIndex = new Random();
-
- this.thread = new Thread(new ThreadStart(this.Work));
- }
-
- /// <summary>
- /// Indicates whether the session is still open
- /// </summary>
- public bool IsOpen
- {
- get { return threadAlive; }
- }
-
- /// <summary>
- /// Start the session in a new thread
- /// </summary>
- public void Start()
- {
- if (!threadAlive) {
- this.thread.Start();
- threadAlive = true;
- }
- }
-
- /// <summary>
- /// Stop the session
- /// </summary>
- public void Stop()
- {
- if (threadAlive) {
- threadAlive = false;
- thread.Abort();
- }
-
- if (controlSocket.Connected)
- controlSocket.Close();
-
- if (dataSocket != null && dataSocket.Connected)
- dataSocket.Close();
- }
-
- /// <summary>
- /// Main method of the session thread.
- /// Reads commands and executes them.
- /// </summary>
- private void Work()
- {
- if (logHandler != null)
- logHandler.NewControlConnection();
-
- try {
- if (!authHandler.AllowControlConnection()) {
- Respond(421, "Control connection refused.");
- // first flush, then close
- controlSocket.Shutdown(SocketShutdown.Both);
- controlSocket.Close();
- return;
- }
-
- Respond(220, String.Format("This is mooftpserv v{0}. {1}", LIB_VERSION, GetRandomText(HELLO_TEXT)));
-
- // allow anonymous login?
- if (authHandler.AllowLogin(null, null)) {
- loggedIn = true;
- }
-
- while (controlSocket.Connected) {
- string verb;
- string args;
- if (!ReadCommand(out verb, out args)) {
- if (controlSocket.Connected) {
- // assume clean disconnect if there are no buffered bytes
- if (cmdRcvBytes != 0)
- Respond(500, "Failed to read command, closing connection.");
- controlSocket.Close();
- }
- break;
- } else if (verb.Trim() == "") {
- // ignore empty lines
- continue;
- }
-
- try {
- if (loggedIn)
- ProcessCommand(verb, args);
- else if (verb == "QUIT") { // QUIT should always be allowed
- Respond(221, "Bye.");
- // first flush, then close
- controlSocket.Shutdown(SocketShutdown.Both);
- controlSocket.Close();
- } else {
- HandleAuth(verb, args);
- }
- } catch (Exception ex) {
- Respond(500, ex);
- }
- }
- } catch (Exception) {
- // catch any uncaught stuff, the server should not throw anything
- } finally {
- if (controlSocket.Connected)
- controlSocket.Close();
-
- if (logHandler != null)
- logHandler.ClosedControlConnection();
-
- threadAlive = false;
- }
- }
-
- /// <summary>
- /// Process an FTP command.
- /// </summary>
- private void ProcessCommand(string verb, string arguments)
- {
- switch (verb) {
- case "SYST":
- {
- Respond(215, "UNIX emulated by mooftpserv");
- break;
- }
- case "QUIT":
- {
- Respond(221, "Bye.");
- // first flush, then close
- controlSocket.Shutdown(SocketShutdown.Both);
- controlSocket.Close();
- break;
- }
- case "USER":
- {
- Respond(230, "You are already logged in.");
- break;
- }
- case "PASS":
- {
- Respond(230, "You are already logged in.");
- break;
- }
- case "FEAT":
- {
- Respond(211, "Features:\r\n " + String.Join("\r\n ", FEATURES), true);
- Respond(211, "Features done.");
- break;
- }
- case "OPTS":
- {
- // Windows Explorer uses lowercase args
- if (arguments != null && arguments.ToUpper() == "UTF8 ON")
- Respond(200, "Always in UTF8 mode.");
- else
- Respond(504, "Unknown option.");
- break;
- }
- case "TYPE":
- {
- if (arguments == "A" || arguments == "A N") {
- transferDataType = DataType.ASCII;
- Respond(200, "Switching to ASCII mode.");
- } else if (arguments == "I") {
- transferDataType = DataType.IMAGE;
- Respond(200, "Switching to BINARY mode.");
- } else {
- Respond(500, "Unknown TYPE arguments.");
- }
- break;
- }
- case "PORT":
- {
- IPEndPoint port = ParseAddress(arguments);
- if (port == null) {
- Respond(500, "Invalid host-port format.");
- break;
- }
-
- if (!authHandler.AllowActiveDataConnection(port)) {
- Respond(500, "PORT arguments refused.");
- break;
- }
-
- dataPort = port;
- CreateDataSocket(false);
- Respond(200, GetRandomText(OK_TEXT));
- break;
- }
- case "PASV":
- {
- dataPort = null;
-
- try {
- CreateDataSocket(true);
- } catch (Exception ex) {
- Respond(500, ex);
- break;
- }
-
- string port = FormatAddress((IPEndPoint) dataSocket.LocalEndPoint);
- Respond(227, String.Format("Switched to passive mode ({0})", port));
- break;
- }
- case "XPWD":
- case "PWD":
- {
- ResultOrError<string> ret = fsHandler.GetCurrentDirectory();
- if (ret.HasError)
- Respond(500, ret.Error);
- else
- Respond(257, EscapePath(ret.Result));
- break;
- }
- case "XCWD":
- case "CWD":
- {
- ResultOrError<string> ret = fsHandler.ChangeDirectory(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(200, GetRandomText(OK_TEXT));
- break;
- }
- case "XCUP":
- case "CDUP":
- {
- ResultOrError<string> ret = fsHandler.ChangeDirectory("..");
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(200, GetRandomText(OK_TEXT));
- break;
- }
- case "XMKD":
- case "MKD":
- {
- ResultOrError<string> ret = fsHandler.CreateDirectory(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(257, EscapePath(ret.Result));
- break;
- }
- case "XRMD":
- case "RMD":
- {
- ResultOrError<bool> ret = fsHandler.RemoveDirectory(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(250, GetRandomText(OK_TEXT));
- break;
- }
- case "RETR":
- {
- ResultOrError<Stream> ret = fsHandler.ReadFile(arguments);
- if (ret.HasError) {
- Respond(550, ret.Error);
- break;
- }
-
- SendData(ret.Result);
- break;
- }
- case "STOR":
- {
- ResultOrError<Stream> ret = fsHandler.WriteFile(arguments);
- if (ret.HasError) {
- Respond(550, ret.Error);
- break;
- }
- ReceiveData(ret.Result);
- var ret2 = fsHandler.WriteFileFinalize(arguments, ret.Result);
- if (ret2.HasError)
- {
- Respond(550, ret.Error);
- break;
- }
- break;
- }
- case "DELE":
- {
- ResultOrError<bool> ret = fsHandler.RemoveFile(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(250, GetRandomText(OK_TEXT));
- break;
- }
- case "RNFR":
- {
- if (arguments == null || arguments.Trim() == "") {
- Respond(500, "Empty path is invalid.");
- break;
- }
-
- renameFromPath = arguments;
- Respond(350, "Waiting for target path.");
- break;
- }
- case "RNTO":
- {
- if (renameFromPath == null) {
- Respond(503, "Use RNFR before RNTO.");
- break;
- }
-
- ResultOrError<bool> ret = fsHandler.RenameFile(renameFromPath, arguments);
- renameFromPath = null;
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(250, GetRandomText(OK_TEXT));
- break;
- }
- case "MDTM":
- {
- ResultOrError<DateTime> ret = fsHandler.GetLastModifiedTimeUtc(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(213, FormatTime(EnsureUnixTime(ret.Result)));
- break;
- }
- case "SIZE":
- {
- ResultOrError<long> ret = fsHandler.GetFileSize(arguments);
- if (ret.HasError)
- Respond(550, ret.Error);
- else
- Respond(213, ret.Result.ToString());
- break;
- }
- case "LIST":
- {
- // apparently browsers like to pass arguments to LIST
- // assuming they are passed through to the UNIX ls command
- arguments = RemoveLsArgs(arguments);
-
- ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
- if (ret.HasError) {
- Respond(500, ret.Error);
- break;
- }
-
- SendData(MakeStream(FormatDirList(ret.Result)));
- break;
- }
- case "STAT":
- {
- if (arguments == null || arguments.Trim() == "") {
- Respond(504, "Not implemented for these arguments.");
- break;
- }
-
- arguments = RemoveLsArgs(arguments);
-
- ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
- if (ret.HasError) {
- Respond(500, ret.Error);
- break;
- }
-
- Respond(213, "Status:\r\n" + FormatDirList(ret.Result), true);
- Respond(213, "Status done.");
- break;
- }
- case "NLST":
- {
- // remove common arguments, we do not support any of them
- arguments = RemoveLsArgs(arguments);
-
- ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
- if (ret.HasError)
- {
- Respond(500, ret.Error);
- break;
- }
-
- SendData(MakeStream(FormatNLST(ret.Result)));
- break;
- }
- case "NOOP":
- {
- Respond(200, GetRandomText(OK_TEXT));
- break;
- }
- default:
- {
- Respond(500, "Unknown command.");
- break;
- }
- }
- }
-
- /// <summary>
- /// Read a command from the control connection.
- /// </summary>
- /// <returns>
- /// True if a command was read.
- /// </returns>
- /// <param name='verb'>
- /// Will receive the verb of the command.
- /// </param>
- /// <param name='args'>
- /// Will receive the arguments of the command, or null.
- /// </param>
- private bool ReadCommand(out string verb, out string args)
- {
- verb = null;
- args = null;
-
- int endPos = -1;
- // can there already be a command in the buffer?
- if (cmdRcvBytes > 0)
- Array.IndexOf(cmdRcvBuffer, (byte)'\n', 0, cmdRcvBytes);
-
- try {
- // read data until a newline is found
- do {
- int freeBytes = cmdRcvBuffer.Length - cmdRcvBytes;
- int bytes = controlSocket.Receive(cmdRcvBuffer, cmdRcvBytes, freeBytes, SocketFlags.None);
- if (bytes <= 0)
- break;
-
- cmdRcvBytes += bytes;
-
- // search \r\n
- endPos = Array.IndexOf(cmdRcvBuffer, (byte)'\r', 0, cmdRcvBytes);
- if (endPos != -1 && (cmdRcvBytes <= endPos + 1 || cmdRcvBuffer[endPos + 1] != (byte)'\n'))
- endPos = -1;
- } while (endPos == -1 && cmdRcvBytes < cmdRcvBuffer.Length);
- } catch (SocketException) {
- // in case the socket is closed or has some other error while reading
- return false;
- }
-
- if (endPos == -1)
- return false;
-
- string command = DecodeString(cmdRcvBuffer, endPos);
-
- // remove the command from the buffer
- cmdRcvBytes -= (endPos + 2);
- Array.Copy(cmdRcvBuffer, endPos + 2, cmdRcvBuffer, 0, cmdRcvBytes);
-
- // CF is missing a limited String.Split
- string[] tokens = command.Split(' ');
- verb = tokens[0].ToUpper(); // commands are case insensitive
- args = (tokens.Length > 1 ? String.Join(" ", tokens, 1, tokens.Length - 1) : null);
-
- if (logHandler != null)
- logHandler.ReceivedCommand(verb, args);
-
- return true;
- }
-
- /// <summary>
- /// Send a response on the control connection
- /// </summary>
- private void Respond(uint code, string desc, bool moreFollows)
- {
- string response = code.ToString();
- if (desc != null)
- response += (moreFollows ? '-' : ' ') + desc;
-
- if (!response.EndsWith("\r\n"))
- response += "\r\n";
-
- byte[] sendBuffer = EncodeString(response);
- controlSocket.Send(sendBuffer);
-
- if (logHandler != null)
- logHandler.SentResponse(code, desc);
- }
-
- /// <summary>
- /// Send a response on the control connection
- /// </summary>
- private void Respond(uint code, string desc)
- {
- Respond(code, desc, false);
- }
-
- /// <summary>
- /// Send a response on the control connection, with an exception as text
- /// </summary>
- private void Respond(uint code, Exception ex)
- {
- Respond(code, ex.Message.Replace(Environment.NewLine, " "));
- }
-
- /// <summary>
- /// Process FTP commands when the user is not yet logged in.
- /// Mostly handles the login commands USER and PASS.
- /// </summary>
- private void HandleAuth(string verb, string args)
- {
- if (verb == "USER" && args != null) {
- if (authHandler.AllowLogin(args, null)) {
- Respond(230, "Login successful.");
- loggedIn = true;
- } else {
- loggedInUser = args;
- Respond(331, "Password please.");
- }
- } else if (verb == "PASS") {
- if (loggedInUser != null) {
- if (authHandler.AllowLogin(loggedInUser, args)) {
- Respond(230, "Login successful.");
- loggedIn = true;
- } else {
- loggedInUser = null;
- Respond(530, "Login failed, please try again.");
- }
- } else {
- Respond(530, "No USER specified.");
- }
- } else {
- Respond(530, "Please login first.");
- }
- }
-
- /// <summary>
- /// Read from the given stream and send the data over a data connection
- /// </summary>
- private void SendData(Stream stream)
- {
- try {
- bool passive = (dataPort == null);
- using (Socket socket = OpenDataConnection()) {
- if (socket == null)
- return;
-
- IPEndPoint remote = (IPEndPoint) socket.RemoteEndPoint;
- IPEndPoint local = (IPEndPoint) socket.LocalEndPoint;
-
- if (logHandler != null)
- logHandler.NewDataConnection(remote, local, passive);
-
- try {
- while (true) {
- int bytes = stream.Read(dataBuffer, 0, dataBufferSize);
- if (bytes <= 0) {
- break;
- }
-
- if (transferDataType == DataType.IMAGE || noAsciiConv) {
- // TYPE I -> just pass through
- socket.Send(dataBuffer, bytes, SocketFlags.None);
- } else {
- // TYPE A -> convert local EOL style to CRLF
-
- // if the buffer ends with a potential partial EOL,
- // try to read the rest of the EOL
- // (i assume that the EOL has max. two bytes)
- if (localEolBytes.Length == 2 &&
- dataBuffer[bytes - 1] == localEolBytes[0]) {
- if (stream.Read(dataBuffer, bytes, 1) == 1)
- ++bytes;
- }
-
- byte[] convBuffer = null;
- int convBytes = ConvertAsciiBytes(dataBuffer, bytes, true, out convBuffer);
- socket.Send(convBuffer, convBytes, SocketFlags.None);
- }
- }
-
- // flush socket before closing (done by using-statement)
- socket.Shutdown(SocketShutdown.Send);
- Respond(226, "Transfer complete.");
- } catch (Exception ex) {
- Respond(500, ex);
- return;
- } finally {
- if (logHandler != null)
- logHandler.ClosedDataConnection(remote, local, passive);
- }
- }
- } finally {
- stream.Close();
- }
- }
-
- /// <summary>
- /// Read from a data connection and write to the given stream
- /// </summary>
- private void ReceiveData(Stream stream)
- {
- try {
- bool passive = (dataPort == null);
- using (Socket socket = OpenDataConnection()) {
- if (socket == null)
- return;
-
- IPEndPoint remote = (IPEndPoint) socket.RemoteEndPoint;
- IPEndPoint local = (IPEndPoint) socket.LocalEndPoint;
-
- if (logHandler != null)
- logHandler.NewDataConnection(remote, local, passive);
-
- try {
- while (true) {
- // fill up the in-memory buffer before writing to disk
- int totalBytes = 0;
- while (totalBytes < dataBufferSize) {
- int freeBytes = dataBufferSize - totalBytes;
- int newBytes = socket.Receive(dataBuffer, totalBytes, freeBytes, SocketFlags.None);
-
- if (newBytes > 0) {
- totalBytes += newBytes;
- } else if (newBytes < 0) {
- Respond(500, String.Format("Transfer failed: Receive() returned {0}", newBytes));
- return;
- } else {
- // end of data
- break;
- }
- }
-
- // end of data
- if (totalBytes == 0)
- break;
-
- if (transferDataType == DataType.IMAGE || noAsciiConv) {
- // TYPE I -> just pass through
- stream.Write(dataBuffer, 0, totalBytes);
- } else {
- // TYPE A -> convert CRLF to local EOL style
-
- // if the buffer ends with a potential partial CRLF,
- // try to read the LF
- if (dataBuffer[totalBytes - 1] == remoteEolBytes[0]) {
- if (socket.Receive(dataBuffer, totalBytes, 1, SocketFlags.None) == 1)
- ++totalBytes;
- }
-
- byte[] convBuffer = null;
- int convBytes = ConvertAsciiBytes(dataBuffer, totalBytes, false, out convBuffer);
- stream.Write(convBuffer, 0, convBytes);
- }
- }
-
- socket.Shutdown(SocketShutdown.Receive);
- Respond(226, "Transfer complete.");
- } catch (Exception ex) {
- Respond(500, ex);
- return;
- } finally {
- if (logHandler != null)
- logHandler.ClosedDataConnection(remote, local, passive);
- }
- }
- } finally {
- //stream.Close();
- }
- }
-
- /// <summary>
- /// Create a socket for a data connection.
- /// </summary>
- /// <param name='listen'>
- /// If true, the socket will be bound to a local port for the PASV command.
- /// Otherwise the socket can be used for connecting to the address given in a PORT command.
- /// </param>
- private void CreateDataSocket(bool listen)
- {
- if (dataSocket != null)
- dataSocket.Close();
-
- dataSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
-
- if (listen) {
- IPAddress serverIP = ((IPEndPoint) controlSocket.LocalEndPoint).Address;
- dataSocket.Bind(new IPEndPoint(serverIP, 0));
- dataSocketBound = true; // CF is missing Socket.IsBound
- dataSocket.Listen(1);
- }
- }
-
- /// <summary>
- /// Opens an active or passive data connection and returns the socket
- /// or null if there was no preceding PORT or PASV command or in case or error.
- /// </summary>
- private Socket OpenDataConnection()
- {
- if (dataPort == null && !dataSocketBound) {
- Respond(425, "No data port configured, use PORT or PASV.");
- return null;
- }
-
- Respond(150, "Opening data connection.");
-
- try {
- if (dataPort != null) {
- // active mode
- dataSocket.Connect(dataPort);
- dataPort = null;
- return dataSocket;
- } else {
- // passive mode
- Socket socket = dataSocket.Accept();
- dataSocket.Close();
- dataSocketBound = false;
- return socket;
- }
- } catch (Exception ex) {
- Respond(500, String.Format("Failed to open data connection: {0}", ex.Message.Replace(Environment.NewLine, " ")));
- return null;
- }
- }
-
- /// <summary>
- /// Convert between different EOL flavors.
- /// </summary>
- /// <returns>
- /// The number of bytes in the resultBuffer.
- /// </returns>
- /// <param name='buffer'>
- /// The input buffer whose data will be converted.
- /// </param>
- /// <param name='len'>
- /// The number of bytes in the input buffer.
- /// </param>
- /// <param name='localToRemote'>
- /// If true, the conversion will be made from local to FTP flavor,
- /// otherwise from FTP to local flavor.
- /// </param>
- /// <param name='resultBuffer'>
- /// The resulting buffer with the converted text.
- /// Can be the same reference as the input buffer if there is nothing to convert.
- /// </param>
- private int ConvertAsciiBytes(byte[] buffer, int len, bool localToRemote, out byte[] resultBuffer)
- {
- byte[] fromBytes = (localToRemote ? localEolBytes : remoteEolBytes);
- byte[] toBytes = (localToRemote ? remoteEolBytes : localEolBytes);
- resultBuffer = null;
-
- int startIndex = 0;
- int resultLen = 0;
- int searchLen;
- while ((searchLen = len - startIndex) > 0) {
- // search for the first byte of the EOL sequence
- int eolIndex = Array.IndexOf(buffer, fromBytes[0], startIndex, searchLen);
-
- // shortcut if there is no EOL in the whole buffer
- if (eolIndex == -1 && startIndex == 0) {
- resultBuffer = buffer;
- return len;
- }
-
- // allocate to worst-case size
- if (resultBuffer == null)
- resultBuffer = new byte[len * 2];
-
- if (eolIndex == -1) {
- Array.Copy(buffer, startIndex, resultBuffer, resultLen, searchLen);
- resultLen += searchLen;
- break;
- } else {
- // compare the rest of the EOL
- int matchBytes = 1;
- for (int i = 1; i < fromBytes.Length && eolIndex + i < len; ++i) {
- if (buffer[eolIndex + i] == fromBytes[i])
- ++matchBytes;
- }
-
- if (matchBytes == fromBytes.Length) {
- // found an EOL to convert
- int copyLen = eolIndex - startIndex;
- if (copyLen > 0) {
- Array.Copy(buffer, startIndex, resultBuffer, resultLen, copyLen);
- resultLen += copyLen;
- }
- Array.Copy(toBytes, 0, resultBuffer, resultLen, toBytes.Length);
- resultLen += toBytes.Length;
- startIndex += copyLen + fromBytes.Length;
- } else {
- int copyLen = (eolIndex - startIndex) + 1;
- Array.Copy(buffer, startIndex, resultBuffer, resultLen, copyLen);
- resultLen += copyLen;
- startIndex += copyLen;
- }
- }
- }
-
- return resultLen;
- }
-
- /// <summary>
- /// Parse the argument of a PORT command into an IPEndPoint
- /// </summary>
- private IPEndPoint ParseAddress(string address)
- {
- string[] tokens = address.Split(',');
- byte[] bytes = new byte[tokens.Length];
- for (int i = 0; i < tokens.Length; ++i) {
- try {
- // CF is missing TryParse
- bytes[i] = byte.Parse(tokens[i]);
- } catch (Exception) {
- return null;
- }
- }
-
- long ip = bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24;
- int port = bytes[4] << 8 | bytes[5];
- return new IPEndPoint(ip, port);
- }
-
- /// <summary>
- /// Format an IPEndPoint so that it can be used in a response for a PASV command
- /// </summary>
- private string FormatAddress(IPEndPoint address)
- {
- byte[] ip = address.Address.GetAddressBytes();
- int port = address.Port;
-
- return String.Format("{0},{1},{2},{3},{4},{5}",
- ip[0], ip[1], ip[2], ip[3],
- (port & 0xFF00) >> 8, port & 0x00FF);
- }
-
- /// <summary>
- /// Formats a list of file system entries for a response to a LIST or STAT command
- /// </summary>
- private string FormatDirList(FileSystemEntry[] list)
- {
- int maxSizeChars = 0;
- foreach (FileSystemEntry entry in list) {
- maxSizeChars = Math.Max(maxSizeChars, entry.Size.ToString().Length);
- }
-
- DateTime sixMonthsAgo = EnsureUnixTime(DateTime.Now.ToUniversalTime().AddMonths(-6));
-
- StringBuilder result = new StringBuilder();
- foreach (FileSystemEntry entry in list) {
- char dirflag = (entry.IsDirectory ? 'd' : '-');
- string size = entry.Size.ToString().PadLeft(maxSizeChars);
- DateTime time = EnsureUnixTime(entry.LastModifiedTimeUtc);
- string timestr = MONTHS[time.Month - 1];
- if (time < sixMonthsAgo)
- timestr += time.ToString(" dd yyyy");
- else
- timestr += time.ToString(" dd hh:mm");
-
- result.AppendFormat("{0}rwxr--r-- 1 owner group {1} {2} {3}\r\n",
- dirflag, size, timestr, entry.Name);
- }
-
- return result.ToString();
- }
-
- /// <summary>
- /// Formats a list of file system entries for a response to an NLST command
- /// </summary>
- private string FormatNLST(FileSystemEntry[] list)
- {
- StringBuilder sb = new StringBuilder();
- foreach (FileSystemEntry entry in list) {
- sb.Append(entry.Name);
- sb.Append("\r\n");
- }
- return sb.ToString();
- }
-
- /// <summary>
- /// Format a timestamp for a reponse to a MDTM command
- /// </summary>
- private string FormatTime(DateTime time)
- {
- return time.ToString("yyyyMMddHHmmss");
- }
-
- /// <summary>
- /// Restrict the year in a timestamp to >= 1970
- /// </summary>
- private DateTime EnsureUnixTime(DateTime time)
- {
- // the server claims to be UNIX, so there should be
- // no timestamps before 1970.
- // e.g. FileZilla does not handle them correctly.
-
- int yearDiff = time.Year - 1970;
- if (yearDiff < 0)
- return time.AddYears(-yearDiff);
- else
- return time;
- }
-
- /// <summary>
- /// Escape a path for a response to a PWD command
- /// </summary>
- private string EscapePath(string path)
- {
- // double-quotes in paths are escaped by doubling them
- return '"' + path.Replace("\"", "\"\"") + '"';
- }
-
- /// <summary>
- /// Remove "-a" or "-l" from the arguments for a LIST or STAT command
- /// </summary>
- private string RemoveLsArgs(string args)
- {
- if (args != null && (args.StartsWith("-a") || args.StartsWith("-l"))) {
- if (args.Length == 2)
- return null;
- else if (args.Length > 3 && args[2] == ' ')
- return args.Substring(3);
- }
-
- return args;
- }
-
- /// <summary>
- /// Convert a string to a list of UTF8 bytes
- /// </summary>
- private byte[] EncodeString(string data)
- {
- return Encoding.UTF8.GetBytes(data);
- }
-
- /// <summary>
- /// Convert a list of UTF8 bytes to a string
- /// </summary>
- private string DecodeString(byte[] data, int len)
- {
- return Encoding.UTF8.GetString(data, 0, len);
- }
-
- /// <summary>
- /// Convert a list of UTF8 bytes to a string
- /// </summary>
- private string DecodeString(byte[] data)
- {
- return DecodeString(data, data.Length);
- }
-
- /// <summary>
- /// Fill a stream with the given string as UTF8 bytes
- /// </summary>
- private Stream MakeStream(string data)
- {
- return new MemoryStream(EncodeString(data));
- }
-
- /// <summary>
- /// Return a randomly selected text from the given list
- /// </summary>
- private string GetRandomText(string[] texts)
- {
- int index = randomTextIndex.Next(0, texts.Length);
- return texts[index];
- }
- }
-}
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// FTP session/connection. Does all the heavy lifting of the FTP protocol.
+ /// Reads commands, sends replies, manages data connections, and so on.
+ /// Each session creates its own thread.
+ /// </summary>
+ class Session
+ {
+ // transfer data type, ascii or binary
+ enum DataType { ASCII, IMAGE };
+
+ // buffer size to use for reading commands from the control connection
+ private static int CMD_BUFFER_SIZE = 4096;
+ // version from AssemblyInfo
+ private static string LIB_VERSION = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(2);
+ // monthnames for LIST command, since DateTime returns localized names
+ private static string[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+ // response text for initial response. preceeded by application name and version number.
+ private static string[] HELLO_TEXT = { "What can I do for you?", "Good day, sir or madam.", "Hey ho let's go!", "The poor man's FTP server." };
+ // response text for general ok messages
+ private static string[] OK_TEXT = { "Sounds good.", "Success!", "Alright, I'll do it...", "Consider it done." };
+ // Result for FEAT command
+ private static string[] FEATURES = { "MDTM", "PASV", "SIZE", "TVFS", "UTF8" };
+
+ // local EOL flavor
+ private static byte[] localEolBytes = Encoding.ASCII.GetBytes(Environment.NewLine);
+ // FTP-mandated EOL flavor (= CRLF)
+ private static byte[] remoteEolBytes = Encoding.ASCII.GetBytes("\r\n");
+ // on Windows, no ASCII conversion is necessary (CRLF == CRLF)
+ private static bool noAsciiConv = (localEolBytes == remoteEolBytes);
+
+ // socket for the control connection
+ private Socket controlSocket;
+ // buffer size to use for sending/receiving with data connections
+ private int dataBufferSize;
+ // auth handler, checks user credentials
+ private IAuthHandler authHandler;
+ // file system handler, implements file system access for the FTP commands
+ private IFileSystemHandler fsHandler;
+ // log handler, used for diagnostic logging output. can be null.
+ private ILogHandler logHandler;
+ // Session thread, the control and data connections are processed in this thread
+ private Thread thread;
+
+ // .NET CF does not have Thread.IsAlive, so this flag replaces it
+ private bool threadAlive = false;
+ // Random Number Generator for OK and HELLO texts
+ private Random randomTextIndex;
+ // flag for whether the user has successfully logged in
+ private bool loggedIn = false;
+ // name of the logged in user, also used to remember the username when waiting for the PASS command
+ private string loggedInUser = null;
+ // argument of pending RNFR command, when waiting for an RNTO command
+ private string renameFromPath = null;
+
+ // remote data port. null when PASV is used.
+ private IPEndPoint dataPort = null;
+ // socket for data connections
+ private Socket dataSocket = null;
+ // .NET CF does not have Socket.Bound, so this flag replaces it
+ private bool dataSocketBound = false;
+ // buffer for reading from the control connection
+ private byte[] cmdRcvBuffer;
+ // number of bytes in the cmdRcvBuffer
+ private int cmdRcvBytes;
+ // buffer for sending/receiving with data connections
+ private byte[] dataBuffer;
+ // data type of the session, can be changed by the client
+ private DataType transferDataType = DataType.ASCII;
+
+ /// <summary>
+ /// Creates a new session, which can afterwards be started with Start().
+ /// </summary>
+ public Session(Socket socket, int bufferSize, IAuthHandler authHandler, IFileSystemHandler fileSystemHandler, ILogHandler logHandler)
+ {
+ this.controlSocket = socket;
+ this.dataBufferSize = bufferSize;
+ this.authHandler = authHandler;
+ this.fsHandler = fileSystemHandler;
+ this.logHandler = logHandler;
+
+ this.cmdRcvBuffer = new byte[CMD_BUFFER_SIZE];
+ this.cmdRcvBytes = 0;
+ this.dataBuffer = new byte[dataBufferSize + 1]; // +1 for partial EOL
+ this.randomTextIndex = new Random();
+
+ this.thread = new Thread(new ThreadStart(this.Work));
+ }
+
+ /// <summary>
+ /// Indicates whether the session is still open
+ /// </summary>
+ public bool IsOpen
+ {
+ get { return threadAlive; }
+ }
+
+ /// <summary>
+ /// Start the session in a new thread
+ /// </summary>
+ public void Start()
+ {
+ if (!threadAlive) {
+ this.thread.Start();
+ threadAlive = true;
+ }
+ }
+
+ /// <summary>
+ /// Stop the session
+ /// </summary>
+ public void Stop()
+ {
+ if (threadAlive) {
+ threadAlive = false;
+ thread.Abort();
+ }
+
+ if (controlSocket.Connected)
+ controlSocket.Close();
+
+ if (dataSocket != null && dataSocket.Connected)
+ dataSocket.Close();
+ }
+
+ /// <summary>
+ /// Main method of the session thread.
+ /// Reads commands and executes them.
+ /// </summary>
+ private void Work()
+ {
+ if (logHandler != null)
+ logHandler.NewControlConnection();
+
+ try {
+ if (!authHandler.AllowControlConnection()) {
+ Respond(421, "Control connection refused.");
+ // first flush, then close
+ controlSocket.Shutdown(SocketShutdown.Both);
+ controlSocket.Close();
+ return;
+ }
+
+ Respond(220, String.Format("This is mooftpserv v{0}. {1}", LIB_VERSION, GetRandomText(HELLO_TEXT)));
+
+ // allow anonymous login?
+ if (authHandler.AllowLogin(null, null)) {
+ loggedIn = true;
+ }
+
+ while (controlSocket.Connected) {
+ string verb;
+ string args;
+ if (!ReadCommand(out verb, out args)) {
+ if (controlSocket.Connected) {
+ // assume clean disconnect if there are no buffered bytes
+ if (cmdRcvBytes != 0)
+ Respond(500, "Failed to read command, closing connection.");
+ controlSocket.Close();
+ }
+ break;
+ } else if (verb.Trim() == "") {
+ // ignore empty lines
+ continue;
+ }
+
+ try {
+ if (loggedIn)
+ ProcessCommand(verb, args);
+ else if (verb == "QUIT") { // QUIT should always be allowed
+ Respond(221, "Bye.");
+ // first flush, then close
+ controlSocket.Shutdown(SocketShutdown.Both);
+ controlSocket.Close();
+ } else {
+ HandleAuth(verb, args);
+ }
+ } catch (Exception ex) {
+ Respond(500, ex);
+ }
+ }
+ } catch (Exception) {
+ // catch any uncaught stuff, the server should not throw anything
+ } finally {
+ if (controlSocket.Connected)
+ controlSocket.Close();
+
+ if (logHandler != null)
+ logHandler.ClosedControlConnection();
+
+ threadAlive = false;
+ }
+ }
+
+ /// <summary>
+ /// Process an FTP command.
+ /// </summary>
+ private void ProcessCommand(string verb, string arguments)
+ {
+ switch (verb) {
+ case "SYST":
+ {
+ Respond(215, "UNIX emulated by mooftpserv");
+ break;
+ }
+ case "QUIT":
+ {
+ Respond(221, "Bye.");
+ // first flush, then close
+ controlSocket.Shutdown(SocketShutdown.Both);
+ controlSocket.Close();
+ break;
+ }
+ case "USER":
+ {
+ Respond(230, "You are already logged in.");
+ break;
+ }
+ case "PASS":
+ {
+ Respond(230, "You are already logged in.");
+ break;
+ }
+ case "FEAT":
+ {
+ Respond(211, "Features:\r\n " + String.Join("\r\n ", FEATURES), true);
+ Respond(211, "Features done.");
+ break;
+ }
+ case "OPTS":
+ {
+ // Windows Explorer uses lowercase args
+ if (arguments != null && arguments.ToUpper() == "UTF8 ON")
+ Respond(200, "Always in UTF8 mode.");
+ else
+ Respond(504, "Unknown option.");
+ break;
+ }
+ case "TYPE":
+ {
+ if (arguments == "A" || arguments == "A N") {
+ transferDataType = DataType.ASCII;
+ Respond(200, "Switching to ASCII mode.");
+ } else if (arguments == "I") {
+ transferDataType = DataType.IMAGE;
+ Respond(200, "Switching to BINARY mode.");
+ } else {
+ Respond(500, "Unknown TYPE arguments.");
+ }
+ break;
+ }
+ case "PORT":
+ {
+ IPEndPoint port = ParseAddress(arguments);
+ if (port == null) {
+ Respond(500, "Invalid host-port format.");
+ break;
+ }
+
+ if (!authHandler.AllowActiveDataConnection(port)) {
+ Respond(500, "PORT arguments refused.");
+ break;
+ }
+
+ dataPort = port;
+ CreateDataSocket(false);
+ Respond(200, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "PASV":
+ {
+ dataPort = null;
+
+ try {
+ CreateDataSocket(true);
+ } catch (Exception ex) {
+ Respond(500, ex);
+ break;
+ }
+
+ string port = FormatAddress((IPEndPoint) dataSocket.LocalEndPoint);
+ Respond(227, String.Format("Switched to passive mode ({0})", port));
+ break;
+ }
+ case "XPWD":
+ case "PWD":
+ {
+ ResultOrError<string> ret = fsHandler.GetCurrentDirectory();
+ if (ret.HasError)
+ Respond(500, ret.Error);
+ else
+ Respond(257, EscapePath(ret.Result));
+ break;
+ }
+ case "XCWD":
+ case "CWD":
+ {
+ ResultOrError<string> ret = fsHandler.ChangeDirectory(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(200, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "XCUP":
+ case "CDUP":
+ {
+ ResultOrError<string> ret = fsHandler.ChangeDirectory("..");
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(200, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "XMKD":
+ case "MKD":
+ {
+ ResultOrError<string> ret = fsHandler.CreateDirectory(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(257, EscapePath(ret.Result));
+ break;
+ }
+ case "XRMD":
+ case "RMD":
+ {
+ ResultOrError<bool> ret = fsHandler.RemoveDirectory(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(250, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "RETR":
+ {
+ ResultOrError<Stream> ret = fsHandler.ReadFile(arguments);
+ if (ret.HasError) {
+ Respond(550, ret.Error);
+ break;
+ }
+
+ SendData(ret.Result);
+ break;
+ }
+ case "STOR":
+ {
+ ResultOrError<Stream> ret = fsHandler.WriteFile(arguments);
+ if (ret.HasError) {
+ Respond(550, ret.Error);
+ break;
+ }
+ ReceiveData(ret.Result);
+ var ret2 = fsHandler.WriteFileFinalize(arguments, ret.Result);
+ if (ret2.HasError)
+ {
+ Respond(550, ret.Error);
+ break;
+ }
+ break;
+ }
+ case "DELE":
+ {
+ ResultOrError<bool> ret = fsHandler.RemoveFile(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(250, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "RNFR":
+ {
+ if (arguments == null || arguments.Trim() == "") {
+ Respond(500, "Empty path is invalid.");
+ break;
+ }
+
+ renameFromPath = arguments;
+ Respond(350, "Waiting for target path.");
+ break;
+ }
+ case "RNTO":
+ {
+ if (renameFromPath == null) {
+ Respond(503, "Use RNFR before RNTO.");
+ break;
+ }
+
+ ResultOrError<bool> ret = fsHandler.RenameFile(renameFromPath, arguments);
+ renameFromPath = null;
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(250, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "MDTM":
+ {
+ ResultOrError<DateTime> ret = fsHandler.GetLastModifiedTimeUtc(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(213, FormatTime(EnsureUnixTime(ret.Result)));
+ break;
+ }
+ case "SIZE":
+ {
+ ResultOrError<long> ret = fsHandler.GetFileSize(arguments);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(213, ret.Result.ToString());
+ break;
+ }
+ case "LIST":
+ {
+ // apparently browsers like to pass arguments to LIST
+ // assuming they are passed through to the UNIX ls command
+ arguments = RemoveLsArgs(arguments);
+
+ ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
+ if (ret.HasError) {
+ Respond(500, ret.Error);
+ break;
+ }
+
+ SendData(MakeStream(FormatDirList(ret.Result)));
+ break;
+ }
+ case "STAT":
+ {
+ if (arguments == null || arguments.Trim() == "") {
+ Respond(504, "Not implemented for these arguments.");
+ break;
+ }
+
+ arguments = RemoveLsArgs(arguments);
+
+ ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
+ if (ret.HasError) {
+ Respond(500, ret.Error);
+ break;
+ }
+
+ Respond(213, "Status:\r\n" + FormatDirList(ret.Result), true);
+ Respond(213, "Status done.");
+ break;
+ }
+ case "NLST":
+ {
+ // remove common arguments, we do not support any of them
+ arguments = RemoveLsArgs(arguments);
+
+ ResultOrError<FileSystemEntry[]> ret = fsHandler.ListEntries(arguments);
+ if (ret.HasError)
+ {
+ Respond(500, ret.Error);
+ break;
+ }
+
+ SendData(MakeStream(FormatNLST(ret.Result)));
+ break;
+ }
+ case "NOOP":
+ {
+ Respond(200, GetRandomText(OK_TEXT));
+ break;
+ }
+ case "SITE":
+ {
+ string[] tokens = arguments.Split(' ');
+ var newverb = tokens[0].ToUpper(); // commands are case insensitive
+ var newargs = (tokens.Length > 1 ? String.Join(" ", tokens, 1, tokens.Length - 1) : null);
+ ProcessCommand(newverb, newargs);
+ break;
+ }
+ case "CHMOD":
+ {
+ string[] tokens = arguments.Split(' ');
+ var mode = tokens[0].ToUpper(); // commands are case insensitive
+ var file = (tokens.Length > 1 ? String.Join(" ", tokens, 1, tokens.Length - 1) : "");
+ ResultOrError<bool> ret = fsHandler.ChmodFile(mode, file);
+ if (ret.HasError)
+ Respond(550, ret.Error);
+ else
+ Respond(250, GetRandomText(OK_TEXT));
+ break;
+ }
+ default:
+ {
+ Respond(500, "Unknown command.");
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Read a command from the control connection.
+ /// </summary>
+ /// <returns>
+ /// True if a command was read.
+ /// </returns>
+ /// <param name='verb'>
+ /// Will receive the verb of the command.
+ /// </param>
+ /// <param name='args'>
+ /// Will receive the arguments of the command, or null.
+ /// </param>
+ private bool ReadCommand(out string verb, out string args)
+ {
+ verb = null;
+ args = null;
+
+ int endPos = -1;
+ // can there already be a command in the buffer?
+ if (cmdRcvBytes > 0)
+ Array.IndexOf(cmdRcvBuffer, (byte)'\n', 0, cmdRcvBytes);
+
+ try {
+ // read data until a newline is found
+ do {
+ int freeBytes = cmdRcvBuffer.Length - cmdRcvBytes;
+ int bytes = controlSocket.Receive(cmdRcvBuffer, cmdRcvBytes, freeBytes, SocketFlags.None);
+ if (bytes <= 0)
+ break;
+
+ cmdRcvBytes += bytes;
+
+ // search \r\n
+ endPos = Array.IndexOf(cmdRcvBuffer, (byte)'\r', 0, cmdRcvBytes);
+ if (endPos != -1 && (cmdRcvBytes <= endPos + 1 || cmdRcvBuffer[endPos + 1] != (byte)'\n'))
+ endPos = -1;
+ } while (endPos == -1 && cmdRcvBytes < cmdRcvBuffer.Length);
+ } catch (SocketException) {
+ // in case the socket is closed or has some other error while reading
+ return false;
+ }
+
+ if (endPos == -1)
+ return false;
+
+ string command = DecodeString(cmdRcvBuffer, endPos);
+
+ // remove the command from the buffer
+ cmdRcvBytes -= (endPos + 2);
+ Array.Copy(cmdRcvBuffer, endPos + 2, cmdRcvBuffer, 0, cmdRcvBytes);
+
+ // CF is missing a limited String.Split
+ string[] tokens = command.Split(' ');
+ verb = tokens[0].ToUpper(); // commands are case insensitive
+ args = (tokens.Length > 1 ? String.Join(" ", tokens, 1, tokens.Length - 1) : null);
+
+ if (logHandler != null)
+ logHandler.ReceivedCommand(verb, args);
+
+ return true;
+ }
+
+ /// <summary>
+ /// Send a response on the control connection
+ /// </summary>
+ private void Respond(uint code, string desc, bool moreFollows)
+ {
+ string response = code.ToString();
+ if (desc != null)
+ response += (moreFollows ? '-' : ' ') + desc;
+
+ if (!response.EndsWith("\r\n"))
+ response += "\r\n";
+
+ byte[] sendBuffer = EncodeString(response);
+ controlSocket.Send(sendBuffer);
+
+ if (logHandler != null)
+ logHandler.SentResponse(code, desc);
+ }
+
+ /// <summary>
+ /// Send a response on the control connection
+ /// </summary>
+ private void Respond(uint code, string desc)
+ {
+ Respond(code, desc, false);
+ }
+
+ /// <summary>
+ /// Send a response on the control connection, with an exception as text
+ /// </summary>
+ private void Respond(uint code, Exception ex)
+ {
+ Respond(code, ex.Message.Replace(Environment.NewLine, " "));
+ }
+
+ /// <summary>
+ /// Process FTP commands when the user is not yet logged in.
+ /// Mostly handles the login commands USER and PASS.
+ /// </summary>
+ private void HandleAuth(string verb, string args)
+ {
+ if (verb == "USER" && args != null) {
+ if (authHandler.AllowLogin(args, null)) {
+ Respond(230, "Login successful.");
+ loggedIn = true;
+ } else {
+ loggedInUser = args;
+ Respond(331, "Password please.");
+ }
+ } else if (verb == "PASS") {
+ if (loggedInUser != null) {
+ if (authHandler.AllowLogin(loggedInUser, args)) {
+ Respond(230, "Login successful.");
+ loggedIn = true;
+ } else {
+ loggedInUser = null;
+ Respond(530, "Login failed, please try again.");
+ }
+ } else {
+ Respond(530, "No USER specified.");
+ }
+ } else {
+ Respond(530, "Please login first.");
+ }
+ }
+
+ /// <summary>
+ /// Read from the given stream and send the data over a data connection
+ /// </summary>
+ private void SendData(Stream stream)
+ {
+ try {
+ bool passive = (dataPort == null);
+ using (Socket socket = OpenDataConnection()) {
+ if (socket == null)
+ return;
+
+ IPEndPoint remote = (IPEndPoint) socket.RemoteEndPoint;
+ IPEndPoint local = (IPEndPoint) socket.LocalEndPoint;
+
+ if (logHandler != null)
+ logHandler.NewDataConnection(remote, local, passive);
+
+ try {
+ while (true) {
+ int bytes = stream.Read(dataBuffer, 0, dataBufferSize);
+ if (bytes <= 0) {
+ break;
+ }
+
+ if (transferDataType == DataType.IMAGE || noAsciiConv) {
+ // TYPE I -> just pass through
+ socket.Send(dataBuffer, bytes, SocketFlags.None);
+ } else {
+ // TYPE A -> convert local EOL style to CRLF
+
+ // if the buffer ends with a potential partial EOL,
+ // try to read the rest of the EOL
+ // (i assume that the EOL has max. two bytes)
+ if (localEolBytes.Length == 2 &&
+ dataBuffer[bytes - 1] == localEolBytes[0]) {
+ if (stream.Read(dataBuffer, bytes, 1) == 1)
+ ++bytes;
+ }
+
+ byte[] convBuffer = null;
+ int convBytes = ConvertAsciiBytes(dataBuffer, bytes, true, out convBuffer);
+ socket.Send(convBuffer, convBytes, SocketFlags.None);
+ }
+ }
+
+ // flush socket before closing (done by using-statement)
+ socket.Shutdown(SocketShutdown.Send);
+ Respond(226, "Transfer complete.");
+ } catch (Exception ex) {
+ Respond(500, ex);
+ return;
+ } finally {
+ if (logHandler != null)
+ logHandler.ClosedDataConnection(remote, local, passive);
+ }
+ }
+ } finally {
+ stream.Close();
+ }
+ }
+
+ /// <summary>
+ /// Read from a data connection and write to the given stream
+ /// </summary>
+ private void ReceiveData(Stream stream)
+ {
+ try {
+ bool passive = (dataPort == null);
+ using (Socket socket = OpenDataConnection()) {
+ if (socket == null)
+ return;
+
+ IPEndPoint remote = (IPEndPoint) socket.RemoteEndPoint;
+ IPEndPoint local = (IPEndPoint) socket.LocalEndPoint;
+
+ if (logHandler != null)
+ logHandler.NewDataConnection(remote, local, passive);
+
+ try {
+ while (true) {
+ // fill up the in-memory buffer before writing to disk
+ int totalBytes = 0;
+ while (totalBytes < dataBufferSize) {
+ int freeBytes = dataBufferSize - totalBytes;
+ int newBytes = socket.Receive(dataBuffer, totalBytes, freeBytes, SocketFlags.None);
+
+ if (newBytes > 0) {
+ totalBytes += newBytes;
+ } else if (newBytes < 0) {
+ Respond(500, String.Format("Transfer failed: Receive() returned {0}", newBytes));
+ return;
+ } else {
+ // end of data
+ break;
+ }
+ }
+
+ // end of data
+ if (totalBytes == 0)
+ break;
+
+ if (transferDataType == DataType.IMAGE || noAsciiConv) {
+ // TYPE I -> just pass through
+ stream.Write(dataBuffer, 0, totalBytes);
+ } else {
+ // TYPE A -> convert CRLF to local EOL style
+
+ // if the buffer ends with a potential partial CRLF,
+ // try to read the LF
+ if (dataBuffer[totalBytes - 1] == remoteEolBytes[0]) {
+ if (socket.Receive(dataBuffer, totalBytes, 1, SocketFlags.None) == 1)
+ ++totalBytes;
+ }
+
+ byte[] convBuffer = null;
+ int convBytes = ConvertAsciiBytes(dataBuffer, totalBytes, false, out convBuffer);
+ stream.Write(convBuffer, 0, convBytes);
+ }
+ }
+
+ socket.Shutdown(SocketShutdown.Receive);
+ Respond(226, "Transfer complete.");
+ } catch (Exception ex) {
+ Respond(500, ex);
+ return;
+ } finally {
+ if (logHandler != null)
+ logHandler.ClosedDataConnection(remote, local, passive);
+ }
+ }
+ } finally {
+ //stream.Close();
+ }
+ }
+
+ /// <summary>
+ /// Create a socket for a data connection.
+ /// </summary>
+ /// <param name='listen'>
+ /// If true, the socket will be bound to a local port for the PASV command.
+ /// Otherwise the socket can be used for connecting to the address given in a PORT command.
+ /// </param>
+ private void CreateDataSocket(bool listen)
+ {
+ if (dataSocket != null)
+ dataSocket.Close();
+
+ dataSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
+
+ if (listen) {
+ IPAddress serverIP = ((IPEndPoint) controlSocket.LocalEndPoint).Address;
+ dataSocket.Bind(new IPEndPoint(serverIP, 0));
+ dataSocketBound = true; // CF is missing Socket.IsBound
+ dataSocket.Listen(1);
+ }
+ }
+
+ /// <summary>
+ /// Opens an active or passive data connection and returns the socket
+ /// or null if there was no preceding PORT or PASV command or in case or error.
+ /// </summary>
+ private Socket OpenDataConnection()
+ {
+ if (dataPort == null && !dataSocketBound) {
+ Respond(425, "No data port configured, use PORT or PASV.");
+ return null;
+ }
+
+ Respond(150, "Opening data connection.");
+
+ try {
+ if (dataPort != null) {
+ // active mode
+ dataSocket.Connect(dataPort);
+ dataPort = null;
+ return dataSocket;
+ } else {
+ // passive mode
+ Socket socket = dataSocket.Accept();
+ dataSocket.Close();
+ dataSocketBound = false;
+ return socket;
+ }
+ } catch (Exception ex) {
+ Respond(500, String.Format("Failed to open data connection: {0}", ex.Message.Replace(Environment.NewLine, " ")));
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Convert between different EOL flavors.
+ /// </summary>
+ /// <returns>
+ /// The number of bytes in the resultBuffer.
+ /// </returns>
+ /// <param name='buffer'>
+ /// The input buffer whose data will be converted.
+ /// </param>
+ /// <param name='len'>
+ /// The number of bytes in the input buffer.
+ /// </param>
+ /// <param name='localToRemote'>
+ /// If true, the conversion will be made from local to FTP flavor,
+ /// otherwise from FTP to local flavor.
+ /// </param>
+ /// <param name='resultBuffer'>
+ /// The resulting buffer with the converted text.
+ /// Can be the same reference as the input buffer if there is nothing to convert.
+ /// </param>
+ private int ConvertAsciiBytes(byte[] buffer, int len, bool localToRemote, out byte[] resultBuffer)
+ {
+ byte[] fromBytes = (localToRemote ? localEolBytes : remoteEolBytes);
+ byte[] toBytes = (localToRemote ? remoteEolBytes : localEolBytes);
+ resultBuffer = null;
+
+ int startIndex = 0;
+ int resultLen = 0;
+ int searchLen;
+ while ((searchLen = len - startIndex) > 0) {
+ // search for the first byte of the EOL sequence
+ int eolIndex = Array.IndexOf(buffer, fromBytes[0], startIndex, searchLen);
+
+ // shortcut if there is no EOL in the whole buffer
+ if (eolIndex == -1 && startIndex == 0) {
+ resultBuffer = buffer;
+ return len;
+ }
+
+ // allocate to worst-case size
+ if (resultBuffer == null)
+ resultBuffer = new byte[len * 2];
+
+ if (eolIndex == -1) {
+ Array.Copy(buffer, startIndex, resultBuffer, resultLen, searchLen);
+ resultLen += searchLen;
+ break;
+ } else {
+ // compare the rest of the EOL
+ int matchBytes = 1;
+ for (int i = 1; i < fromBytes.Length && eolIndex + i < len; ++i) {
+ if (buffer[eolIndex + i] == fromBytes[i])
+ ++matchBytes;
+ }
+
+ if (matchBytes == fromBytes.Length) {
+ // found an EOL to convert
+ int copyLen = eolIndex - startIndex;
+ if (copyLen > 0) {
+ Array.Copy(buffer, startIndex, resultBuffer, resultLen, copyLen);
+ resultLen += copyLen;
+ }
+ Array.Copy(toBytes, 0, resultBuffer, resultLen, toBytes.Length);
+ resultLen += toBytes.Length;
+ startIndex += copyLen + fromBytes.Length;
+ } else {
+ int copyLen = (eolIndex - startIndex) + 1;
+ Array.Copy(buffer, startIndex, resultBuffer, resultLen, copyLen);
+ resultLen += copyLen;
+ startIndex += copyLen;
+ }
+ }
+ }
+
+ return resultLen;
+ }
+
+ /// <summary>
+ /// Parse the argument of a PORT command into an IPEndPoint
+ /// </summary>
+ private IPEndPoint ParseAddress(string address)
+ {
+ string[] tokens = address.Split(',');
+ byte[] bytes = new byte[tokens.Length];
+ for (int i = 0; i < tokens.Length; ++i) {
+ try {
+ // CF is missing TryParse
+ bytes[i] = byte.Parse(tokens[i]);
+ } catch (Exception) {
+ return null;
+ }
+ }
+
+ long ip = bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24;
+ int port = bytes[4] << 8 | bytes[5];
+ return new IPEndPoint(ip, port);
+ }
+
+ /// <summary>
+ /// Format an IPEndPoint so that it can be used in a response for a PASV command
+ /// </summary>
+ private string FormatAddress(IPEndPoint address)
+ {
+ byte[] ip = address.Address.GetAddressBytes();
+ int port = address.Port;
+
+ return String.Format("{0},{1},{2},{3},{4},{5}",
+ ip[0], ip[1], ip[2], ip[3],
+ (port & 0xFF00) >> 8, port & 0x00FF);
+ }
+
+ /// <summary>
+ /// Formats a list of file system entries for a response to a LIST or STAT command
+ /// </summary>
+ private string FormatDirList(FileSystemEntry[] list)
+ {
+ int maxSizeChars = 0;
+ foreach (FileSystemEntry entry in list) {
+ maxSizeChars = Math.Max(maxSizeChars, entry.Size.ToString().Length);
+ }
+
+ DateTime sixMonthsAgo = EnsureUnixTime(DateTime.Now.ToUniversalTime().AddMonths(-6));
+
+ StringBuilder result = new StringBuilder();
+ foreach (FileSystemEntry entry in list) {
+ char dirflag = (entry.IsDirectory ? 'd' : '-');
+ string size = entry.Size.ToString().PadLeft(maxSizeChars);
+ DateTime time = EnsureUnixTime(entry.LastModifiedTimeUtc);
+ string timestr = MONTHS[time.Month - 1];
+ if (time < sixMonthsAgo)
+ timestr += time.ToString(" dd yyyy");
+ else
+ timestr += time.ToString(" dd hh:mm");
+ string mode = entry.Mode;
+
+ result.AppendFormat("{0}{4} 1 owner group {1} {2} {3}\r\n",
+ dirflag, size, timestr, entry.Name, mode ?? "rwxr--r--");
+ }
+
+ return result.ToString();
+ }
+
+ /// <summary>
+ /// Formats a list of file system entries for a response to an NLST command
+ /// </summary>
+ private string FormatNLST(FileSystemEntry[] list)
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (FileSystemEntry entry in list) {
+ sb.Append(entry.Name);
+ sb.Append("\r\n");
+ }
+ return sb.ToString();
+ }
+
+ /// <summary>
+ /// Format a timestamp for a reponse to a MDTM command
+ /// </summary>
+ private string FormatTime(DateTime time)
+ {
+ return time.ToString("yyyyMMddHHmmss");
+ }
+
+ /// <summary>
+ /// Restrict the year in a timestamp to >= 1970
+ /// </summary>
+ private DateTime EnsureUnixTime(DateTime time)
+ {
+ // the server claims to be UNIX, so there should be
+ // no timestamps before 1970.
+ // e.g. FileZilla does not handle them correctly.
+
+ int yearDiff = time.Year - 1970;
+ if (yearDiff < 0)
+ return time.AddYears(-yearDiff);
+ else
+ return time;
+ }
+
+ /// <summary>
+ /// Escape a path for a response to a PWD command
+ /// </summary>
+ private string EscapePath(string path)
+ {
+ // double-quotes in paths are escaped by doubling them
+ return '"' + path.Replace("\"", "\"\"") + '"';
+ }
+
+ /// <summary>
+ /// Remove "-a" or "-l" from the arguments for a LIST or STAT command
+ /// </summary>
+ private string RemoveLsArgs(string args)
+ {
+ if (args != null && (args.StartsWith("-a") || args.StartsWith("-l"))) {
+ if (args.Length == 2)
+ return null;
+ else if (args.Length > 3 && args[2] == ' ')
+ return args.Substring(3);
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Convert a string to a list of UTF8 bytes
+ /// </summary>
+ private byte[] EncodeString(string data)
+ {
+ return Encoding.UTF8.GetBytes(data);
+ }
+
+ /// <summary>
+ /// Convert a list of UTF8 bytes to a string
+ /// </summary>
+ private string DecodeString(byte[] data, int len)
+ {
+ return Encoding.UTF8.GetString(data, 0, len);
+ }
+
+ /// <summary>
+ /// Convert a list of UTF8 bytes to a string
+ /// </summary>
+ private string DecodeString(byte[] data)
+ {
+ return DecodeString(data, data.Length);
+ }
+
+ /// <summary>
+ /// Fill a stream with the given string as UTF8 bytes
+ /// </summary>
+ private Stream MakeStream(string data)
+ {
+ return new MemoryStream(EncodeString(data));
+ }
+
+ /// <summary>
+ /// Return a randomly selected text from the given list
+ /// </summary>
+ private string GetRandomText(string[] texts)
+ {
+ int index = randomTextIndex.Next(0, texts.Length);
+ return texts[index];
+ }
+ }
+}