using System; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace mooftpserv { /// /// 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. /// 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 = { "hakchi2 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", "MLST modify*;perm*;size*;type*;unique*;UNIX.mode;", "PASV", "MFMT", "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; /// /// Creates a new session, which can afterwards be started with Start(). /// 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)); } /// /// Indicates whether the session is still open /// public bool IsOpen { get { return threadAlive; } } /// /// Start the session in a new thread /// public void Start() { if (!threadAlive) { this.thread.Start(); threadAlive = true; } } /// /// Stop the session /// public void Stop() { if (threadAlive) { threadAlive = false; thread.Abort(); } if (controlSocket.Connected) controlSocket.Close(); if (dataSocket != null && dataSocket.Connected) dataSocket.Close(); } /// /// Main method of the session thread. /// Reads commands and executes them. /// 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; } } /// /// Process an FTP command. /// 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 ret = fsHandler.GetCurrentDirectory(); if (ret.HasError) Respond(500, ret.Error); else Respond(257, EscapePath(ret.Result)); break; } case "XCWD": case "CWD": { ResultOrError ret = fsHandler.ChangeDirectory(arguments); if (ret.HasError) Respond(550, ret.Error); else Respond(200, GetRandomText(OK_TEXT)); break; } case "XCUP": case "CDUP": { ResultOrError ret = fsHandler.ChangeDirectory(".."); if (ret.HasError) Respond(550, ret.Error); else Respond(200, GetRandomText(OK_TEXT)); break; } case "XMKD": case "MKD": { ResultOrError ret = fsHandler.CreateDirectory(arguments); if (ret.HasError) Respond(550, ret.Error); else Respond(257, EscapePath(ret.Result)); break; } case "XRMD": case "RMD": { ResultOrError ret = fsHandler.RemoveDirectory(arguments); if (ret.HasError) Respond(550, ret.Error); else Respond(250, GetRandomText(OK_TEXT)); break; } case "RETR": { ResultOrError ret = fsHandler.ReadFile(arguments); if (ret.HasError) { Respond(550, ret.Error); break; } SendData(ret.Result); break; } case "STOR": { ResultOrError 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, ret2.Error); break; } break; } case "DELE": { ResultOrError 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 ret = fsHandler.RenameFile(renameFromPath, arguments); renameFromPath = null; if (ret.HasError) Respond(550, ret.Error); else Respond(250, GetRandomText(OK_TEXT)); break; } case "MDTM": { ResultOrError ret = fsHandler.GetLastModifiedTimeUtc(arguments); if (ret.HasError) Respond(550, ret.Error); else Respond(213, FormatTime(EnsureUnixTime(ret.Result))); break; } case "SIZE": { ResultOrError 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 ret = fsHandler.ListEntries(arguments); if (ret.HasError) { Respond(500, ret.Error); break; } SendData(MakeStream(FormatDirList(ret.Result))); */ ResultOrError ret = fsHandler.ListEntriesRaw(arguments); if (ret.HasError) { Respond(500, ret.Error); break; } SendData(MakeStream(ret.Result)); break; } case "STAT": { if (arguments == null || arguments.Trim() == "") { Respond(504, "Not implemented for these arguments."); break; } arguments = RemoveLsArgs(arguments); ResultOrError 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 ret = fsHandler.ListEntries(arguments); if (ret.HasError) { Respond(500, ret.Error); break; } SendData(MakeStream(FormatNLST(ret.Result))); break; } case "MLSD": case "MLST": { ResultOrError ret = fsHandler.ListEntries(arguments); if (ret.HasError) { Respond(500, ret.Error); break; } SendData(MakeStream(FormatMLST(ret.Result))); break; } case "MFMT": { string[] tokens = arguments.Split(' '); var time = DateTime.ParseExact(tokens[0], "yyyyMMddHHmmss", CultureInfo.InvariantCulture); var file = (tokens.Length > 1 ? String.Join(" ", tokens, 1, tokens.Length - 1) : null); fsHandler.SetLastModifiedTimeUtc(file, time); Respond(213, string.Format("213 Modify={0}; {1}", tokens[0], file)); 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 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; } } } /// /// Read a command from the control connection. /// /// /// True if a command was read. /// /// /// Will receive the verb of the command. /// /// /// Will receive the arguments of the command, or null. /// 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; } /// /// Send a response on the control connection /// 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); } /// /// Send a response on the control connection /// private void Respond(uint code, string desc) { Respond(code, desc, false); } /// /// Send a response on the control connection, with an exception as text /// private void Respond(uint code, Exception ex) { Respond(code, ex.Message.Replace(Environment.NewLine, " ")); } /// /// Process FTP commands when the user is not yet logged in. /// Mostly handles the login commands USER and PASS. /// 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."); } } /// /// Read from the given stream and send the data over a data connection /// 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(); } } /// /// Read from a data connection and write to the given stream /// 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(); } } /// /// Create a socket for a data connection. /// /// /// 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. /// 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); } } /// /// 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. /// 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; } } /// /// Convert between different EOL flavors. /// /// /// The number of bytes in the resultBuffer. /// /// /// The input buffer whose data will be converted. /// /// /// The number of bytes in the input buffer. /// /// /// If true, the conversion will be made from local to FTP flavor, /// otherwise from FTP to local flavor. /// /// /// The resulting buffer with the converted text. /// Can be the same reference as the input buffer if there is nothing to convert. /// 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; } /// /// Parse the argument of a PORT command into an IPEndPoint /// 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); } /// /// Format an IPEndPoint so that it can be used in a response for a PASV command /// 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); } /// /// Formats a list of file system entries for a response to a LIST or STAT command /// 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; if (string.IsNullOrEmpty(mode)) mode = dirflag + "rwxr--r--"; result.AppendFormat("{0} 1 owner group {1} {2} {3}\r\n", mode, size, timestr, entry.Name); } return result.ToString(); } /// /// Formats a list of file system entries for a response to an NLST command /// 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(); } /// /// Formats a list of file system entries for a response to an MLST command /// private string FormatMLST(FileSystemEntry[] list) { StringBuilder sb = new StringBuilder(); var cd = fsHandler.GetCurrentDirectory(); foreach (FileSystemEntry entry in list) { int p = entry.Name.IndexOf(" -> "); string l, f; if (p >= 0) { l = entry.Name.Substring(0, p); f = entry.Name.Substring(p + 4); } else { l = f = entry.Name; } sb.AppendFormat("modify={0:yyyyMMddHHmmss};perm={1};size={2};type={3};unique={4:X};unix.mode={5:D4}; {6}\r\n", entry.LastModifiedTimeUtc, "rw" + (entry.IsDirectory ? "l" : "") + (entry.Mode != null && entry.Mode.Contains("x") ? "x" : ""), entry.Size, (l != f) ? "symlink" : (entry.IsDirectory ? "dir" : "file"), (cd.Result + f).GetHashCode(), (string.IsNullOrEmpty(entry.Mode) || entry.Mode.Length < 10) ? 0 : ( ((entry.Mode[3] == 'S') ? 4000 : 0) + ((entry.Mode[6] == 'S') ? 2000 : 0) + ((entry.Mode[9] == 'T') ? 1000 : 0) + ((entry.Mode[1] == 'r') ? 400 : 0) + ((entry.Mode[2] == 'w') ? 200 : 0) + ((entry.Mode[3] != '-') ? 100 : 0) + ((entry.Mode[4] == 'r') ? 040 : 0) + ((entry.Mode[5] == 'w') ? 020 : 0) + ((entry.Mode[6] != '-') ? 010 : 0) + ((entry.Mode[7] == 'r') ? 004 : 0) + ((entry.Mode[8] == 'w') ? 002 : 0) + ((entry.Mode[9] != '-') ? 001 : 0) ), l); sb.Append("\r\n"); } return sb.ToString(); } /// /// Format a timestamp for a reponse to a MDTM command /// private string FormatTime(DateTime time) { return time.ToString("yyyyMMddHHmmss"); } /// /// Restrict the year in a timestamp to >= 1970 /// 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; } /// /// Escape a path for a response to a PWD command /// private string EscapePath(string path) { // double-quotes in paths are escaped by doubling them return '"' + path.Replace("\"", "\"\"") + '"'; } /// /// Remove "-a" or "-l" from the arguments for a LIST or STAT command /// 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; } /// /// Convert a string to a list of UTF8 bytes /// private byte[] EncodeString(string data) { return Encoding.UTF8.GetBytes(data); } /// /// Convert a list of UTF8 bytes to a string /// private string DecodeString(byte[] data, int len) { return Encoding.UTF8.GetString(data, 0, len); } /// /// Convert a list of UTF8 bytes to a string /// private string DecodeString(byte[] data) { return DecodeString(data, data.Length); } /// /// Fill a stream with the given string as UTF8 bytes /// private Stream MakeStream(string data) { return new MemoryStream(EncodeString(data)); } /// /// Return a randomly selected text from the given list /// private string GetRandomText(string[] texts) { int index = randomTextIndex.Next(0, texts.Length); return texts[index]; } } }