Optional interface that is used to provide processing for the FTP SITE command.
+ */
+public interface FTPSiteInterface {
+
+ /**
+ * Process an FTP SITE specific command
+ *
+ * @param sess FTPSrvSession
+ * @param req FTPRequest
+ */
+ void processFTPSiteCommand( FTPSrvSession sess, FTPRequest req);
+}
diff --git a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java
index db56f00801..540afa1106 100644
--- a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java
+++ b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java
@@ -26,9 +26,11 @@ import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.StringTokenizer;
+import java.util.TimeZone;
import java.util.Vector;
import javax.transaction.UserTransaction;
@@ -57,6 +59,7 @@ import org.alfresco.filesys.server.filesys.SrvDiskInfo;
import org.alfresco.filesys.server.filesys.TreeConnection;
import org.alfresco.filesys.server.filesys.TreeConnectionHash;
import org.alfresco.filesys.smb.server.repo.ContentContext;
+import org.alfresco.filesys.util.HexDump;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -105,6 +108,13 @@ public class FTPSrvSession extends SrvSession implements Runnable
public static final int DBG_DIRECTORY = 0x00000200; // Directory commands
+ // Enabled features
+
+ protected static boolean FeatureUTF8 = false;
+ protected static boolean FeatureMDTM = true;
+ protected static boolean FeatureSIZE = true;
+ protected static boolean FeatureMLST = true;
+
// Anonymous user name
private static final String USER_ANONYMOUS = "anonymous";
@@ -133,8 +143,36 @@ public class FTPSrvSession extends SrvSession implements Runnable
// LIST command options
- protected final static String LIST_OPTION_HIDDEN = "-a";
+ protected final static String LIST_OPTION_PREFIX = "-";
+ protected final static char LIST_OPTION_HIDDEN = 'a';
+
+ // Machine listing fact ids
+
+ protected static final int MLST_SIZE = 0x0001;
+ protected static final int MLST_MODIFY = 0x0002;
+ protected static final int MLST_CREATE = 0x0004;
+ protected static final int MLST_TYPE = 0x0008;
+ protected static final int MLST_UNIQUE = 0x0010;
+ protected static final int MLST_PERM = 0x0020;
+ protected static final int MLST_MEDIATYPE = 0x0040;
+
+ // Default fact list to use for machine listing commands
+
+ protected static final int MLST_DEFAULT = MLST_SIZE + MLST_MODIFY + MLST_CREATE + MLST_TYPE + MLST_UNIQUE + MLST_PERM + MLST_MEDIATYPE;
+
+ // Machine listing fact names
+
+ protected static final String _factNames[] = { "size", "modify", "create", "type", "unique", "perm", "media-type" };
+
+ // MLSD buffer size to allocate
+
+ protected static final int MLSD_BUFFER_SIZE = 4096;
+
+ // Modify date/time minimum date/time argument length
+
+ protected static final int MDTM_DATETIME_MINLEN = 14; // YYYYMMDDHHMMSS
+
// Flag to control whether data transfers use a seperate thread
private static boolean UseThreadedDataTransfer = false;
@@ -145,12 +183,12 @@ public class FTPSrvSession extends SrvSession implements Runnable
// Input/output streams to remote client
- private InputStreamReader m_in;
-
- private char[] m_inbuf;
+ private InputStream m_in;
+ private byte[] m_inbuf;
+// private InputStreamReader m_in;
+// private char[] m_inbuf;
private OutputStreamWriter m_out;
-
private StringBuffer m_outbuf;
// Data connection
@@ -171,6 +209,14 @@ public class FTPSrvSession extends SrvSession implements Runnable
private long m_restartPos = 0;
+ // Flag to indicate if UTF-8 paths are enabled
+
+ private boolean m_utf8Paths = false;
+
+ // Machine listing fact list
+
+ private int m_mlstFacts = MLST_DEFAULT;
+
// Rename from path details
private FTPPath m_renameFrom;
@@ -183,6 +229,26 @@ public class FTPSrvSession extends SrvSession implements Runnable
private TreeConnectionHash m_connections;
+ /**
+ * Static initializer
+ */
+ static
+ {
+ try
+ {
+ // Check if the sun.text classes are available for UTF-8 conversion
+
+ Class.forName( "sun.text.Normalizer");
+
+ // Enable UTF-8 support
+
+ FeatureUTF8 = true;
+ }
+ catch ( Exception ex)
+ {
+ }
+ }
+
/**
* Class constructor
*
@@ -338,6 +404,16 @@ public class FTPSrvSession extends SrvSession implements Runnable
return m_cwd != null ? true : false;
}
+ /**
+ * Check if UTF-8 filenames are enabled
+ *
+ * @return boolean
+ */
+ public final boolean isUTF8Enabled()
+ {
+ return m_utf8Paths;
+ }
+
/**
* Set the default path for the session
*
@@ -388,7 +464,7 @@ public class FTPSrvSession extends SrvSession implements Runnable
// Convert the path to an FTP format path
- String path = convertToFTPSeperators(req.getArgument());
+ String path = convertToFTPSeperators( req.getArgument());
// Check if the path is the root directory and there is a default root
// path configured
@@ -1205,23 +1281,28 @@ public class FTPSrvSession extends SrvSession implements Runnable
boolean hidden = false;
- if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_HIDDEN))
+ if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_PREFIX))
{
- // Indicate that we want hidden files in the listing
-
- hidden = true;
-
- // Remove the option from the command argument, and update the
- // request
-
- String arg = req.getArgument();
- int pos = arg.indexOf(" ");
- if (pos > 0)
- arg = arg.substring(pos + 1);
- else
- arg = null;
-
- req.updateArgument(arg);
+ // We only support the hidden files option
+
+ String arg = req.getArgument();
+ if ( arg.indexOf( LIST_OPTION_HIDDEN) != -1)
+ {
+ // Indicate that we want hidden files in the listing
+
+ hidden = true;
+ }
+
+ // Remove the option from the command argument, and update the
+ // request
+
+ int pos = arg.indexOf(" ");
+ if (pos > 0)
+ arg = arg.substring(pos + 1);
+ else
+ arg = null;
+
+ req.updateArgument(arg);
}
// Create the path for the file listing
@@ -1294,7 +1375,10 @@ public class FTPSrvSession extends SrvSession implements Runnable
// Open an output stream to the client
- dataWrt = new OutputStreamWriter(dataSock.getOutputStream());
+ if ( isUTF8Enabled())
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8");
+ else
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream());
// Check if a path has been specified to list
@@ -1472,7 +1556,10 @@ public class FTPSrvSession extends SrvSession implements Runnable
// Open an output stream to the client
- dataWrt = new OutputStreamWriter(dataSock.getOutputStream());
+ if ( isUTF8Enabled())
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8");
+ else
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream());
// Check if a path has been specified to list
@@ -1593,11 +1680,177 @@ public class FTPSrvSession extends SrvSession implements Runnable
}
/**
- * Process a quit command
- *
- * @param req FTPRequest
- * @exception IOException
- */
+ * Process an options request
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
+ protected final void procOptions(FTPRequest req) throws IOException {
+
+ // Check if the user is logged in
+
+ if (isLoggedOn() == false)
+ {
+ sendFTPResponse(500, "");
+ return;
+ }
+
+ // Check if the parameter has been specified
+
+ if (req.hasArgument() == false)
+ {
+ sendFTPResponse(501, "Required argument missing");
+ return;
+ }
+
+ // Parse the argument to get the sub-command and arguments
+
+ StringTokenizer token = new StringTokenizer(req.getArgument(), " ");
+ if (token.hasMoreTokens() == false)
+ {
+ sendFTPResponse(501, "Invalid argument");
+ return;
+ }
+
+ // Get the sub-command
+
+ String optsCmd = token.nextToken();
+
+ // UTF8 enable/disable command
+
+ if (FeatureUTF8 && optsCmd.equalsIgnoreCase("UTF8"))
+ {
+
+ // Get the next argument
+
+ if (token.hasMoreTokens())
+ {
+ String optsArg = token.nextToken();
+ if (optsArg.equalsIgnoreCase("ON"))
+ {
+
+ // Enable UTF-8 file names
+
+ m_utf8Paths = true;
+ }
+ else if (optsArg.equalsIgnoreCase("OFF"))
+ {
+
+ // Disable UTF-8 file names
+
+ m_utf8Paths = false;
+ }
+ else
+ {
+
+ // Invalid argument
+
+ sendFTPResponse(501, "OPTS UTF8 Invalid argument");
+ return;
+ }
+
+ // Report the new setting back to the client
+
+ sendFTPResponse(200, "OPTS UTF8 " + (isUTF8Enabled() ? "ON" : "OFF"));
+
+ // DEBUG
+
+ if (logger.isDebugEnabled() && hasDebug(DBG_FILE))
+ logger.debug("UTF8 options utf8=" + (isUTF8Enabled() ? "ON" : "OFF"));
+ }
+ }
+
+ // MLST/MLSD fact list command
+
+ else if (FeatureMLST && optsCmd.equalsIgnoreCase("MLST"))
+ {
+
+ // Check if the fact list argument is valid
+
+ if (token.hasMoreTokens() == false)
+ {
+
+ // Invalid fact list argument
+
+ sendFTPResponse(501, "OPTS MLST Invalid argument");
+ return;
+ }
+
+ // Parse the supplied fact names
+
+ int mlstFacts = 0;
+ StringTokenizer factTokens = new StringTokenizer(token.nextToken(),
+ ";");
+ StringBuffer factStr = new StringBuffer();
+
+ while (factTokens.hasMoreTokens())
+ {
+ // Get the current fact name and validate
+
+ String factName = factTokens.nextToken();
+ int factIdx = -1;
+ int idx = 0;
+
+ while (idx < _factNames.length && factIdx == -1)
+ {
+ if (_factNames[idx].equalsIgnoreCase(factName))
+ factIdx = idx;
+ else
+ idx++;
+ }
+
+ // Check if the fact name is valid, ignore invalid names
+
+ if (factIdx != -1)
+ {
+ // Add the fact name to the reply tring
+
+ factStr.append(_factNames[factIdx]);
+ factStr.append(";");
+
+ // Add the fact to the fact bit mask
+
+ mlstFacts += (1 << factIdx);
+ }
+ }
+
+ // check if any valid fact names were found
+
+ if (mlstFacts == 0)
+ {
+ sendFTPResponse(501, "OPTS MLST Invalid Argument");
+ return;
+ }
+
+ // Update the MLST enabled fact list for this session
+
+ m_mlstFacts = mlstFacts;
+
+ // Send the response
+
+ sendFTPResponse(200, "MLST OPTS " + factStr.toString());
+
+ // DEBUG
+
+ if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH))
+ logger.debug("MLst options facts=" + factStr.toString());
+ }
+ else
+ {
+ // Invalid options command, or feature not enabled
+
+ sendFTPResponse(501, "Invalid options commands");
+ }
+ }
+
+ /**
+ * Process a quit command
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
protected final void procQuit(FTPRequest req) throws IOException
{
@@ -1616,11 +1869,12 @@ public class FTPSrvSession extends SrvSession implements Runnable
}
/**
- * Process a type command
- *
- * @param req FTPRequest
- * @exception IOException
- */
+ * Process a type command
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
protected final void procType(FTPRequest req) throws IOException
{
@@ -1984,9 +2238,10 @@ public class FTPSrvSession extends SrvSession implements Runnable
* Process a store file command
*
* @param req FTPRequest
+ * @param append boolean
* @exception IOException
*/
- protected final void procStoreFile(FTPRequest req) throws IOException
+ protected final void procStoreFile(FTPRequest req, boolean append) throws IOException
{
// Check if the user is logged in
@@ -2072,9 +2327,11 @@ public class FTPSrvSession extends SrvSession implements Runnable
// Create the file open parameters
- FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(),
- sts == FileStatus.FileExists ? FileAction.TruncateExisting : FileAction.CreateNotExist,
- AccessMode.ReadWrite, 0);
+ int openAction = FileAction.CreateNotExist;
+ if ( sts == FileStatus.FileExists)
+ openAction = append == false ? FileAction.TruncateExisting : FileAction.OpenIfExists;
+
+ FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), openAction, AccessMode.ReadWrite, 0);
// Create a new file to receive the data
@@ -2163,6 +2420,11 @@ public class FTPSrvSession extends SrvSession implements Runnable
long filePos = 0;
int len = is.read(buf, 0, buf.length);
+ // If the data is to be appended then set the starting file position to the end of the file
+
+ if ( append == true)
+ filePos = netFile.getFileSize();
+
while (len > 0)
{
@@ -2775,9 +3037,7 @@ public class FTPSrvSession extends SrvSession implements Runnable
}
// Check if the path is the root directory, cannot delete directories
- // from the root
- // directory
- // as it maps to the list of available disk shares.
+ // from the root directory as it maps to the list of available disk shares.
if (ftpPath.isRootPath() || ftpPath.isRootSharePath())
{
@@ -2820,8 +3080,7 @@ public class FTPSrvSession extends SrvSession implements Runnable
disk.deleteDirectory(this, tree, ftpPath.getSharePath());
- // Check if there are any file/directory change notify requests
- // active
+ // Check if there are any file/directory change notify requests active
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
@@ -2857,48 +3116,474 @@ public class FTPSrvSession extends SrvSession implements Runnable
}
/**
- * Process a modify date/time command
- *
- * @param req FTPRequest
- * @exception IOException
- */
- protected final void procModifyDateTime(FTPRequest req) throws IOException
- {
+ * Process a machine listing request, single folder
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
+ protected final void procMachineListing(FTPRequest req) throws IOException {
- // Return a success response
+ // Check if the user is logged in
- sendFTPResponse(550, "Not implemented yet");
- }
+ if (isLoggedOn() == false) {
+ sendFTPResponse(500, "Not logged in");
+ return;
+ }
- /**
- * Process a features command
- *
- * @param req FTPRequest
- * @exception IOException
- */
- protected final void procFeatures(FTPRequest req) throws IOException
- {
- // Check if the user is logged in
+ // Check if an argument has been specified
- if (isLoggedOn() == false)
- {
- sendFTPResponse(500, "");
- return;
- }
+ if (req.hasArgument() == false) {
+ sendFTPResponse(501, "Syntax error, parameter required");
+ return;
+ }
- // Send back the list of features supported by this FTP server
-
- sendFTPResponse( 211, "Features");
- sendFTPResponse( "SIZE");
- sendFTPResponse( 211, "End");
- }
+ // Create the path to be listed
+
+ FTPPath ftpPath = generatePathForRequest(req, false, true);
+ if (ftpPath == null) {
+ sendFTPResponse(500, "Invalid path");
+ return;
+ }
+
+ // Get the file information
+
+ DiskInterface disk = null;
+ TreeConnection tree = null;
+
+ try {
+
+ // Create a temporary tree connection
+
+ tree = getTreeConnection(ftpPath.getSharedDevice());
+
+ // Access the virtual filesystem driver
+
+ disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
+
+ // Get the file information
+
+ FileInfo finfo = disk.getFileInformation(this, tree, ftpPath
+ .getSharePath());
+
+ if (finfo == null) {
+ sendFTPResponse(550, "Path " + req.getArgument() + " not available");
+ return;
+ } else if (finfo.isDirectory() == false) {
+ sendFTPResponse(501, "Path " + req.getArgument() + " is not a directory");
+ return;
+ }
+
+ // Return the folder details
+
+ sendFTPResponse("250- Listing " + req.getArgument());
+
+ StringBuffer mlstStr = new StringBuffer(80);
+ mlstStr.append(" ");
+
+ generateMlstString(finfo, m_mlstFacts, mlstStr, true);
+ mlstStr.append(CRLF);
+
+ sendFTPResponse(mlstStr.toString());
+ sendFTPResponse("250 End");
+
+ // DEBUG
+
+ if ( logger.isDebugEnabled() && hasDebug(DBG_FILE))
+ logger.debug("Mlst ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", info=" + finfo);
+ } catch (Exception ex) {
+ sendFTPResponse(550, "Error retrieving file information");
+ }
+ }
+
+ /**
+ * Process a machine listing request, folder contents
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
+ protected final void procMachineListingContents(FTPRequest req)
+ throws IOException {
+
+ // Check if the user is logged in
+
+ if (isLoggedOn() == false) {
+ sendFTPResponse(500, "");
+ return;
+ }
+
+ // Check if the request has an argument, if not then use the current
+ // working directory
+
+ if (req.hasArgument() == false)
+ req.updateArgument(".");
+
+ // Create the path for the file listing
+
+ FTPPath ftpPath = m_cwd;
+ if (req.hasArgument())
+ ftpPath = generatePathForRequest(req, true);
+
+ if (ftpPath == null) {
+ sendFTPResponse(500, "Invalid path");
+ return;
+ }
+
+ // Check if the session has the required access
+
+ if (ftpPath.isRootPath() == false) {
+
+ // Check if the session has access to the filesystem
+
+ TreeConnection tree = getTreeConnection(ftpPath.getSharedDevice());
+ if (tree == null || tree.hasReadAccess() == false) {
+
+ // Session does not have access to the filesystem
+
+ sendFTPResponse(550, "Access denied");
+ return;
+ }
+ }
+
+ // Send the intermediate response
+
+ sendFTPResponse(150, "File status okay, about to open data connection");
+
+ // Check if there is an active data session
+
+ if (m_dataSess == null) {
+ sendFTPResponse(425, "Can't open data connection");
+ return;
+ }
+
+ // Get the data connection socket
+
+ Socket dataSock = null;
+
+ try {
+ dataSock = m_dataSess.getSocket();
+ } catch (Exception ex) {
+ logger.error(ex);
+ }
+
+ if (dataSock == null) {
+ sendFTPResponse(426, "Connection closed; transfer aborted");
+ return;
+ }
+
+ // Output the directory listing to the client
+
+ Writer dataWrt = null;
+
+ try {
+
+ // Open an output stream to the client
+
+ if ( isUTF8Enabled())
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream(), "UTF-8");
+ else
+ dataWrt = new OutputStreamWriter(dataSock.getOutputStream());
+
+ // Get a list of file information objects for the current directory
+
+ Vector files = null;
+
+ files = listFilesForPath(ftpPath, false, false);
+
+ // Output the file list to the client
+
+ if (files != null) {
+
+ // DEBUG
+
+ if (logger.isDebugEnabled() && hasDebug(DBG_SEARCH))
+ logger.debug("MLsd found " + files.size() + " files in " + ftpPath.getFTPPath());
+
+ // Output the file information to the client
+
+ StringBuffer str = new StringBuffer(MLSD_BUFFER_SIZE);
+
+ for (int i = 0; i < files.size(); i++) {
+
+ // Get the current file information
+
+ FileInfo finfo = (FileInfo) files.elementAt(i);
+
+ generateMlstString(finfo, m_mlstFacts, str, false);
+ str.append(CRLF);
+
+ // Output the file information record when the buffer is
+ // full
+
+ if (str.length() >= MLSD_BUFFER_SIZE) {
+
+ // Output the file data records
+
+ dataWrt.write(str.toString());
+
+ // Reset the buffer
+
+ str.setLength(0);
+ }
+ }
+
+ // Flush any remaining file record data
+
+ if (str.length() > 0)
+ dataWrt.write(str.toString());
+ }
+
+ // End of file list transmission
+
+ sendFTPResponse(226, "Closing data connection");
+ } catch (Exception ex) {
+
+ // Failed to send file listing
+
+ sendFTPResponse(451, "Error reading file list");
+ } finally {
+
+ // Close the data stream to the client
+
+ if (dataWrt != null)
+ dataWrt.close();
+
+ // Close the data connection to the client
+
+ if (m_dataSess != null) {
+ getFTPServer().releaseDataSession(m_dataSess);
+ m_dataSess = null;
+ }
+ }
+ }
+
+ /**
+ * Process a modify date/time command
+ *
+ * @param req
+ * FTPRequest
+ * @exception IOException
+ */
+ protected final void procModifyDateTime(FTPRequest req) throws IOException {
+
+ // Check if the user is logged in
+
+ if (isLoggedOn() == false) {
+ sendFTPResponse(500, "");
+ return;
+ }
+
+ // Check if an argument has been specified
+
+ if (req.hasArgument() == false) {
+ sendFTPResponse(501, "Syntax error, parameter required");
+ return;
+ }
+
+ // Check the format of the argument to detemine if this is a get or set
+ // modify date/time request
+ //
+ // Get format is just the filename/path
+ // Set format is YYYYMMDDHHMMSS