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

github.com/ClusterM/hakchi2.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-11 07:32:55 +0300
committerAlexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com>2017-04-11 07:32:55 +0300
commit96e1f17aa9e5f7397726fd478f08c229cc727d3c (patch)
tree4df6ff581f4b4229c9d77acfa7135ae2b627e41c /FtpServer
parentbafb7face696afa218eb7105cb814f170c6b54b0 (diff)
FTP server (seriously!) and many fixes
Diffstat (limited to 'FtpServer')
-rw-r--r--FtpServer/DebugLogHandler.cs74
-rw-r--r--FtpServer/FileSystemHelper.cs66
-rw-r--r--FtpServer/IAuthHandler.cs45
-rw-r--r--FtpServer/IFileSystemHandler.cs202
-rw-r--r--FtpServer/ILogHandler.cs25
-rw-r--r--FtpServer/NesMiniAuthHandler.cs41
-rw-r--r--FtpServer/NesMiniFileSystemHandler.cs317
-rw-r--r--FtpServer/Server.cs190
-rw-r--r--FtpServer/Session.cs1045
9 files changed, 2005 insertions, 0 deletions
diff --git a/FtpServer/DebugLogHandler.cs b/FtpServer/DebugLogHandler.cs
new file mode 100644
index 00000000..292d5eee
--- /dev/null
+++ b/FtpServer/DebugLogHandler.cs
@@ -0,0 +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
+ }
+ }
+}
+
diff --git a/FtpServer/FileSystemHelper.cs b/FtpServer/FileSystemHelper.cs
new file mode 100644
index 00000000..36cc4979
--- /dev/null
+++ b/FtpServer/FileSystemHelper.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+
+namespace mooftpserv
+{
+ public class FileSystemHelper
+ {
+ /// Handles TVFS path resolution, similar to Path.GetFullPath(Path.Combine(basePath, path))
+ public static string ResolvePath(string basePath, string path)
+ {
+ // CF is missing String.IsNullOrWhiteSpace
+ if (path == null || path.Trim() == "")
+ return null;
+
+ // first, make a complete unix path
+ string fullPath;
+ if (path[0] == '/') {
+ fullPath = path;
+ } else {
+ fullPath = basePath;
+ if (!fullPath.EndsWith("/"))
+ fullPath += "/";
+ fullPath += path;
+ }
+
+ // then, remove ".." and "."
+ List<string> tokens = new List<string>(fullPath.Split('/'));
+ for (int i = 0; i < tokens.Count; ++i) {
+ if (tokens[i] == "") {
+ if (i == 0 || i == tokens.Count - 1) {
+ continue; // ignore, start and end should be empty tokens
+ } else {
+ tokens.RemoveAt(i);
+ --i;
+ }
+ } else if (tokens[i] == "..") {
+ if (i < 2) {
+ // cannot go higher than root, just remove the token
+ tokens.RemoveAt(i);
+ --i;
+ } else {
+ tokens.RemoveRange(i - 1, 2);
+ i -= 2;
+ }
+ } else if (i < tokens.Count - 1 && tokens[i].EndsWith(@"\")) {
+ int slashes = 0;
+ for (int c = tokens[i].Length - 1; c >= 0 && tokens[i][c] == '\\'; --c)
+ ++slashes;
+
+ if (slashes % 2 != 0) {
+ // the slash was actually escaped, merge tokens
+ tokens[i] += ("/" + tokens[i + 1]);
+ ++i;
+ }
+ }
+ }
+
+ if (tokens.Count > 1)
+ return String.Join("/", tokens.ToArray());
+ else
+ return "/";
+ }
+
+ }
+}
+
diff --git a/FtpServer/IAuthHandler.cs b/FtpServer/IAuthHandler.cs
new file mode 100644
index 00000000..24621c87
--- /dev/null
+++ b/FtpServer/IAuthHandler.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Net;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// Interface for a class managing user authentication and allowing connections.
+ /// </summary>
+ public interface IAuthHandler
+ {
+ /// <summary>
+ /// Make a new instance for a new session with the given peer.
+ /// Each FTP session uses a separate, cloned instance.
+ /// </summary>
+ IAuthHandler Clone(IPEndPoint peer);
+
+ /// <summary>
+ /// Check the given login. Note that the method can be called in three ways:
+ /// - user and pass are null: anonymous authentication
+ /// - pass is null: login only with username (e.g. "anonymous")
+ /// - both are non-null: login with user and password
+ /// </summary>
+ /// <param name='user'>
+ /// The username, or null.
+ /// </param>
+ /// <param name='pass'>
+ /// The password, or null.
+ /// </param>
+ bool AllowLogin(string user, string pass);
+
+ /// <summary>
+ /// Check if a control connection from the peer should be allowed.
+ /// </summary>
+ bool AllowControlConnection();
+
+ /// <summary>
+ /// Check if the PORT command of the peer with the given
+ /// target endpoint should be allowed.
+ /// </summary>
+ /// The argument given by the peer in the PORT command.
+ /// </param>
+ bool AllowActiveDataConnection(IPEndPoint target);
+ }
+}
+
diff --git a/FtpServer/IFileSystemHandler.cs b/FtpServer/IFileSystemHandler.cs
new file mode 100644
index 00000000..c84e8426
--- /dev/null
+++ b/FtpServer/IFileSystemHandler.cs
@@ -0,0 +1,202 @@
+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);
+ }
+}
+
diff --git a/FtpServer/ILogHandler.cs b/FtpServer/ILogHandler.cs
new file mode 100644
index 00000000..87b7c9ad
--- /dev/null
+++ b/FtpServer/ILogHandler.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Net;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// Interface for a logger. Methods should be self-explanatory.
+ /// </summary>
+ public interface ILogHandler
+ {
+ /// <summary>
+ /// Make a new instance for a new session with the given peer.
+ /// Each FTP session uses a separate, cloned instance.
+ /// </summary>
+ ILogHandler Clone(IPEndPoint peer);
+
+ void NewControlConnection();
+ void ClosedControlConnection();
+ void ReceivedCommand(string verb, string arguments);
+ void SentResponse(uint code, string description);
+ void NewDataConnection(IPEndPoint remote, IPEndPoint local, bool passive);
+ void ClosedDataConnection(IPEndPoint remote, IPEndPoint local, bool passive);
+ }
+}
+
diff --git a/FtpServer/NesMiniAuthHandler.cs b/FtpServer/NesMiniAuthHandler.cs
new file mode 100644
index 00000000..3da4c2b7
--- /dev/null
+++ b/FtpServer/NesMiniAuthHandler.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Net;
+
+namespace mooftpserv
+{
+ public class NesMiniAuthHandler : IAuthHandler
+ {
+ private IPEndPoint peer;
+
+ public NesMiniAuthHandler()
+ {
+ }
+
+ private NesMiniAuthHandler(IPEndPoint peer)
+ {
+ this.peer = peer;
+ }
+
+ public IAuthHandler Clone(IPEndPoint peer)
+ {
+ return new NesMiniAuthHandler(peer);
+ }
+
+ public bool AllowLogin(string user, string pass)
+ {
+ return (user == "root" && pass == "clover");
+ }
+
+ public bool AllowControlConnection()
+ {
+ return true;
+ }
+
+ public bool AllowActiveDataConnection(IPEndPoint port)
+ {
+ // only allow active connections to the same peer as the control connection
+ return peer.Address.Equals(port.Address);
+ }
+ }
+}
+
diff --git a/FtpServer/NesMiniFileSystemHandler.cs b/FtpServer/NesMiniFileSystemHandler.cs
new file mode 100644
index 00000000..63b8b1cb
--- /dev/null
+++ b/FtpServer/NesMiniFileSystemHandler.cs
@@ -0,0 +1,317 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.IO;
+using com.clusterrr.clovershell;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// Default file system handler. Allows access to the whole file system. Supports drives on Windows.
+ /// </summary>
+ 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);
+ }
+ }
+}
diff --git a/FtpServer/Server.cs b/FtpServer/Server.cs
new file mode 100644
index 00000000..b544f8e4
--- /dev/null
+++ b/FtpServer/Server.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+
+namespace mooftpserv
+{
+ /// <summary>
+ /// Main FTP server class. Manages the server socket, creates sessions.
+ /// Can be used to configure the server.
+ /// </summary>
+ public class Server
+ {
+ // default buffer size for send/receive buffers
+ private const int DEFAULT_BUFFER_SIZE = 64 * 1024;
+ // default port for the server socket
+ private const int DEFAULT_PORT = 21;
+
+ private IPEndPoint endpoint;
+ private int bufferSize = DEFAULT_BUFFER_SIZE;
+ private IAuthHandler authHandler = null;
+ private IFileSystemHandler fsHandler = null;
+ private ILogHandler logHandler = null;
+ private TcpListener socket = null;
+ private List<Session> sessions;
+
+ public Server()
+ {
+ this.endpoint = new IPEndPoint(GetDefaultAddress(), DEFAULT_PORT);
+ this.sessions = new List<Session>();
+ }
+
+ /// <summary>
+ /// Gets or sets the local end point on which the server will listen.
+ /// Has to be an IPv4 endpoint.
+ /// The default value is IPAdress.Any and port 21, except on WinCE,
+ /// where the first non-loopback IPv4 address will be used.
+ /// </summary>
+ public IPEndPoint LocalEndPoint
+ {
+ get { return endpoint; }
+ set { endpoint = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the local IP address on which the server will listen.
+ /// Has to be an IPv4 address.
+ /// If none is set, IPAddress.Any will be used, except on WinCE,
+ /// where the first non-loopback IPv4 address will be used.
+ /// </summary>
+ public IPAddress LocalAddress
+ {
+ get { return endpoint.Address; }
+ set { endpoint.Address = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the local port on which the server will listen.
+ /// The default value is 21. Note that on Linux, only root can open ports < 1024.
+ /// </summary>
+ public int LocalPort
+ {
+ get { return endpoint.Port; }
+ set { endpoint.Port = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the size of the send/receive buffer to be used by each session/connection.
+ /// The default value is 64k.
+ /// </summary>
+ public int BufferSize
+ {
+ get { return bufferSize; }
+ set { bufferSize = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the auth handler that is used to check user credentials.
+ /// If none is set, a DefaultAuthHandler will be created when the server starts.
+ /// </summary>
+ public IAuthHandler AuthHandler
+ {
+ get { return authHandler; }
+ set { authHandler = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the file system handler that implements file system access for FTP commands.
+ /// If none is set, a DefaultFileSystemHandler is created when the server starts.
+ /// </summary>
+ public IFileSystemHandler FileSystemHandler
+ {
+ get { return fsHandler; }
+ set { fsHandler = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the log handler. Can be null to disable logging.
+ /// The default value is null.
+ /// </summary>
+ public ILogHandler LogHandler
+ {
+ get { return logHandler; }
+ set { logHandler = value; }
+ }
+
+ /// <summary>
+ /// Run the server. The method will not return until Stop() is called.
+ /// </summary>
+ public void Run()
+ {
+ //if (authHandler == null)
+ // authHandler = new DefaultAuthHandler();
+
+ //if (fsHandler == null)
+ // fsHandler = new DefaultFileSystemHandler();
+
+ if (socket == null)
+ socket = new TcpListener(endpoint);
+
+ socket.Start();
+
+ // listen for new connections
+ try {
+ while (true)
+ {
+ Socket peer = socket.AcceptSocket();
+
+ IPEndPoint peerPort = (IPEndPoint) peer.RemoteEndPoint;
+ Session session = new Session(peer, bufferSize,
+ authHandler.Clone(peerPort),
+ fsHandler.Clone(peerPort),
+ logHandler.Clone(peerPort));
+
+ session.Start();
+ sessions.Add(session);
+
+ // purge old sessions
+ for (int i = sessions.Count - 1; i >= 0; --i)
+ {
+ if (!sessions[i].IsOpen) {
+ sessions.RemoveAt(i);
+ --i;
+ }
+ }
+ }
+ } catch (SocketException) {
+ // ignore, Stop() will probably cause this exception
+ } finally {
+ // close all running connections
+ foreach (Session s in sessions) {
+ s.Stop();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stop the server.
+ /// </summary>
+ public void Stop()
+ {
+ if (socket == null) return;
+ socket.Stop();
+ }
+
+ /// <summary>
+ /// Get the default address, which is IPAddress.Any everywhere except on WinCE,
+ /// where all local addresses are enumerated and the first non-loopback IP is used.
+ /// </summary>
+ private IPAddress GetDefaultAddress()
+ {
+ // on WinCE, 0.0.0.0 does not work because for accepted sockets,
+ // LocalEndPoint would also say 0.0.0.0 instead of the real IP
+#if WindowsCE
+ IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
+ IPAddress bindIp = IPAddress.Loopback;
+ foreach (IPAddress ip in host.AddressList) {
+ if (ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip)) {
+ return ip;
+ }
+ }
+
+ return IPAddress.Loopback;
+#else
+ return IPAddress.Any;
+#endif
+ }
+ }
+}
diff --git a/FtpServer/Session.cs b/FtpServer/Session.cs
new file mode 100644
index 00000000..068156d5
--- /dev/null
+++ b/FtpServer/Session.cs
@@ -0,0 +1,1045 @@
+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];
+ }
+ }
+}