org.filesys.ftp.FTPSrvSession Maven / Gradle / Ivy
Show all versions of jfileserver Show documentation
/*
* Copyright (C) 2006-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.filesys.ftp;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.*;
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import org.filesys.debug.Debug;
import org.filesys.server.SrvSession;
import org.filesys.server.auth.ClientInfo;
import org.filesys.server.auth.ISMBAuthenticator;
import org.filesys.server.auth.acl.AccessControl;
import org.filesys.server.auth.acl.AccessControlManager;
import org.filesys.server.core.ShareType;
import org.filesys.server.core.SharedDevice;
import org.filesys.server.core.SharedDeviceList;
import org.filesys.server.filesys.*;
import org.filesys.util.MemorySize;
import org.filesys.util.UTF8Normalizer;
import org.filesys.util.WildCard;
/**
* FTP Server Session Class
*
* @author gkspencer
*/
public class FTPSrvSession extends SrvSession implements Runnable {
// Constants
//
// Debug flags
public enum Dbg {
STATE, // Session state changes
RXDATA, // Received data
TXDATA, // Transmit data
DUMPDATA, // Dump data packets
SEARCH, // File/directory search
INFO, // Information requests
FILE, // File open/close/info
FILEIO, // File read/write
ERROR, // Errors
PKTTYPE, // Received packet type
TIMING, // Time packet processing
DATAPORT, // Data port
DIRECTORY, // Directory commands
SSL // Secure sessions
}
// Enabled features
public static final boolean FeatureUTF8 = true;
public static final boolean FeatureMFMT = true;
public static final boolean FeatureSIZE = true;
public static final boolean FeatureMLST = true;
public static final boolean FeatureAUTH = true;
// Root directory and FTP directory seperator
private static final String ROOT_DIRECTORY = "/";
private static final String FTP_SEPERATOR = "/";
private static final char FTP_SEPERATOR_CHAR = '/';
// Share relative path directory seperator
private static final String DIR_SEPERATOR = "\\";
private static final char DIR_SEPERATOR_CHAR = '\\';
// File transfer buffer size
private static final int DEFAULT_BUFFERSIZE = (int) (256 * MemorySize.KILOBYTE);
// Carriage return/line feed combination required for response messages
protected final static String CRLF = "\r\n";
// LIST command options
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
// Network address types, for EPRT and EPSV commands
protected static final int TypeIPv4 = 1;
protected static final int TypeIPv6 = 2;
// Valid protection levels for PROT command
protected static final String ProtLevels = "CSEP";
protected static final String ProtLevelClear = "C";
// Maximum size to extend the command buffer to
protected static final int DefCommandBufSize = 1024;
protected static final int MaxCommandBufSize = 0xFFFF; // 64K
// Session socket
private Socket m_sock;
// Input/output streams to remote client
private InputStream m_in;
private byte[] m_inbuf;
private OutputStreamWriter m_out;
// List of pending FTP commands
private List m_ftpCmdList;
// Data connection
private FTPDataSession m_dataSess;
// Current working directory details
//
// First level is the share name then a path relative to the share root
private FTPPath m_cwd;
// Binary mode flag
private boolean m_binary = false;
// Restart position for binary file transfer
private long m_restartPos = 0;
// Flag to indicate if UTF-8 paths are enabled
private boolean m_utf8Paths = true;
private UTF8Normalizer m_normalizer;
// Machine listing fact list
private int m_mlstFacts = MLST_DEFAULT;
// Rename from path details
private FTPPath m_renameFrom;
// Filtered list of shared filesystems available to this session
private SharedDeviceList m_shares;
// List of shared device connections used by this session
private TreeConnectionHash m_connections;
// SSL/TLS testing
private SSLContext m_sslContext;
private SSLEngine m_sslEngine;
private ByteBuffer m_sslIn;
private ByteBuffer m_sslOut;
// Protected buffer size and protection level
private int m_pbSize = -1;
private String m_protLevel;
/**
* Class constructor
*
* @param sock Socket
* @param srv FTPServer
*/
public FTPSrvSession(Socket sock, FTPServer srv) {
super(-1, srv, "FTP", null, FTPSrvSession.Dbg.class);
// Save the local socket
m_sock = sock;
// Set the socket linger options, so the socket closes immediately when closed
try {
m_sock.setSoLinger(false, 0);
}
catch (SocketException ex) {
}
// Indicate that the user is not logged in
setLoggedOn(false);
// Allocate the FTP path
m_cwd = new FTPPath();
// Allocate the tree connection cache
m_connections = new TreeConnectionHash();
// Allocate the command list
m_ftpCmdList = new ArrayList();
// Get the UTF-8 string normalizer, if available
m_normalizer = srv.getUTF8Normalizer();
}
/**
* Close the FTP session, and associated data socket if active
*/
public final void closeSession() {
// Call the base class
super.closeSession();
// Close the data connection, if active
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// Check if there is an active transaction
if (hasTransaction()) {
// DEBUG
if (Debug.EnableError)
debugPrintln("** Active transaction after packet processing, cleaning up **");
// Close the active transaction
endTransaction();
}
// Close the socket first, if the client is still connected this should allow the
// input/output streams to be closed
if (m_sock != null) {
try {
m_sock.close();
}
catch (Exception ex) {
}
m_sock = null;
}
// Close the input/output streams
if (m_in != null) {
try {
m_in.close();
}
catch (Exception ex) {
}
m_in = null;
}
if (m_out != null) {
try {
m_out.close();
}
catch (Exception ex) {
}
m_out = null;
}
// Remove session from server session list
getFTPServer().removeSession(this);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Session closed, " + getSessionId());
}
/**
* Return the current working directory
*
* @return String
*/
public final String getCurrentWorkingDirectory() {
return m_cwd.getFTPPath();
}
/**
* Return the server that this session is associated with.
*
* @return FTPServer
*/
public final FTPServer getFTPServer() {
return (FTPServer) getServer();
}
/**
* Return the client network address
*
* @return InetAddress
*/
public final InetAddress getRemoteAddress() {
return m_sock.getInetAddress();
}
/**
* Check if there is a current working directory
*
* @return boolean
*/
public final boolean hasCurrentWorkingDirectory() {
return m_cwd != null ? true : false;
}
/**
* Check if UTF-8 filenames are enabled
*
* @return boolean
*/
public final boolean isUTF8Enabled() {
return (m_utf8Paths == true && m_normalizer != null);
}
/**
* Set the default path for the session
*
* @param rootPath FTPPath
*/
public final void setRootPath(FTPPath rootPath) {
// Initialize the current working directory using the root path
m_cwd = new FTPPath(rootPath);
m_cwd.setSharedDevice(getShareList(), this);
}
/**
* Get the path details for the current request
*
* @param req FTPRequest
* @param filePath boolean
* @return FTPPath
*/
protected final FTPPath generatePathForRequest(FTPRequest req, boolean filePath) {
return generatePathForRequest(req, filePath, true);
}
/**
* Get the path details for the current request
*
* @param req FTPRequest
* @param filePath boolean
* @param checkExists boolean
* @return FTPPath
*/
protected final FTPPath generatePathForRequest(FTPRequest req, boolean filePath, boolean checkExists) {
// Convert the path from UTF-8, if enabled
String path = req.getArgument();
// Convert the path to an FTP format path
path = convertToFTPSeperators(path);
// Check if the path is the root directory and there is a default root path configured
FTPPath ftpPath = null;
if (path.compareTo(ROOT_DIRECTORY) == 0) {
// Check if the FTP server has a default root directory configured
FTPServer ftpSrv = (FTPServer) getServer();
if (ftpSrv.hasRootPath())
ftpPath = ftpSrv.getRootPath();
else {
try {
ftpPath = new FTPPath("/");
}
catch (Exception ex) {
}
return ftpPath;
}
}
// Check if the path is relative
else if (FTPPath.isRelativePath(path) == false) {
// Create a new path for the directory
try {
ftpPath = new FTPPath(path);
}
catch (InvalidPathException ex) {
return null;
}
// Find the associated shared device
if (ftpPath.setSharedDevice(getShareList(), this) == false)
return null;
} else {
// Check for the special '.' directory, just return the current working directory
if (path.equals(".") || path.length() == 0)
return m_cwd;
// Check for the special '..' directory, if already at the root directory return an
// error
if (path.equals("..")) {
// Check if we are already at the root path
if (m_cwd.isRootPath() == false) {
// Remove the last directory from the path
m_cwd.removeDirectory();
m_cwd.setSharedDevice(getShareList(), this);
// Return the new path
return m_cwd;
} else
return null;
}
// Create a copy of the current working directory and append the new file/directory name
ftpPath = new FTPPath(m_cwd);
// Check if the root directory/share has been set
if (ftpPath.isRootPath()) {
// Path specifies the share name and possibly a subdirectory
try {
ftpPath.setFTPPath(FTP_SEPERATOR + path);
}
catch (InvalidPathException ex) {
return null;
}
} else {
if (filePath)
ftpPath.addFile(path);
else
ftpPath.addDirectory(path);
}
// Find the associated shared device, if not already set
if (ftpPath.hasSharedDevice() == false && ftpPath.setSharedDevice(getShareList(), this) == false)
return null;
}
// Check if the generated path exists, if the share path is not an empty string or the root folder ('\')
if (checkExists && ftpPath.hasSharePath() && ftpPath.getSharePath().length() > 1) {
// Check if the new path exists and is a directory
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();
// Check if the path exists
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.NotExist) {
// Get the path string, check if there is a leading seperator
String pathStr = req.getArgument();
if (pathStr.startsWith(FTP_SEPERATOR) == false)
pathStr = FTP_SEPERATOR + pathStr;
// Create the root path
ftpPath = new FTPPath(pathStr);
// Find the associated shared device
if (ftpPath.setSharedDevice(getShareList(), this) == false)
ftpPath = null;
// Check the path again
if (disk.fileExists(this, tree, ftpPath.getSharePath()) == FileStatus.NotExist)
ftpPath = null;
} else if ((sts == FileStatus.FileExists && filePath == false)
|| (sts == FileStatus.DirectoryExists && filePath == true)) {
// Path exists but is the wrong type (directory or file)
ftpPath = null;
}
}
catch (Exception ex) {
ftpPath = null;
}
}
// Return the new path
return ftpPath;
}
/**
* Convert a path string from share path seperators to FTP path seperators
*
* @param path String
* @return String
*/
protected final String convertToFTPSeperators(String path) {
// Check if the path is valid
if (path == null || path.indexOf(DIR_SEPERATOR) == -1)
return path;
// Replace the path seperators
return path.replace(DIR_SEPERATOR_CHAR, FTP_SEPERATOR_CHAR);
}
/**
* Find the required disk shared device
*
* @param name String
* @return DiskSharedDevice
*/
protected final DiskSharedDevice findShare(String name) {
// Check if the name is valid
if (name == null)
return null;
// Find the required disk share
SharedDevice shr = getFTPServer().getShareList().findShare(m_cwd.getShareName(), ShareType.DISK, true);
if (shr != null && shr instanceof DiskSharedDevice)
return (DiskSharedDevice) shr;
// Disk share not found
return null;
}
/**
* Set the binary mode flag
*
* @param bin boolean
*/
protected final void setBinary(boolean bin) {
m_binary = bin;
}
/**
* Send an FTP command response
*
* @param stsCode int
* @param msg String
* @exception IOException Socket error
*/
public final void sendFTPResponse(int stsCode, String msg)
throws IOException {
// Build the output record
StringBuffer outbuf = new StringBuffer(10 + (msg != null ? msg.length() : 0));
outbuf.append(stsCode);
outbuf.append(" ");
if (msg != null)
outbuf.append(msg);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TXDATA))
debugPrintln("Tx msg=" + outbuf.toString());
if (Debug.EnableError && hasDebug(FTPSrvSession.Dbg.ERROR) && stsCode >= 500)
debugPrintln("Error status=" + stsCode + ", msg=" + msg);
// Add the CR/LF
outbuf.append(CRLF);
// Output the FTP response
if (m_out != null) {
// Check if the response should be encrypted
if (m_sslEngine != null) {
// Encrypt the response
sendEncryptedFTPResponse(outbuf.toString());
} else {
// Plaintext connection
m_out.write(outbuf.toString());
m_out.flush();
}
}
}
/**
* Send an unencrypted FTP command response
*
* @param stsCode int
* @param msg String
* @exception IOException Socket error
*/
public final void sendUnencryptedFTPResponse(int stsCode, String msg)
throws IOException {
// Build the output record
StringBuffer outbuf = new StringBuffer(10 + (msg != null ? msg.length() : 0));
outbuf.append(stsCode);
outbuf.append(" ");
if (msg != null)
outbuf.append(msg);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TXDATA))
debugPrintln("Tx msg=" + outbuf.toString());
if (Debug.EnableError && hasDebug(FTPSrvSession.Dbg.ERROR) && stsCode >= 500)
debugPrintln("Error status=" + stsCode + ", msg=" + msg);
// Add the CR/LF
outbuf.append(CRLF);
// Output the FTP response
if (m_out != null) {
m_out.write(outbuf.toString());
m_out.flush();
}
}
/**
* Send an FTP command response
*
* @param msg StringBuffer
* @exception IOException Socket error
*/
public final void sendFTPResponse(StringBuffer msg)
throws IOException {
sendFTPResponse(msg.toString());
}
/**
* Send an FTP command response
*
* @param msg String
* @exception IOException Socket error
*/
public final void sendFTPResponse(String msg)
throws IOException {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TXDATA))
debugPrintln("Tx msg=" + msg);
// Output the FTP response
if (m_out != null) {
// Check if the response should be encrypted
if (m_sslEngine != null) {
// Encrypt the response
StringBuilder str = new StringBuilder(msg.length() + CRLF.length());
str.append(msg);
str.append(CRLF);
sendEncryptedFTPResponse(str.toString());
} else {
// Plaintext connection
m_out.write(msg);
m_out.write(CRLF);
m_out.flush();
}
}
}
/**
* Send an encrypted FTP response
*
* @param msg String
* @exception IOException Socket error
*/
protected final void sendEncryptedFTPResponse(String msg)
throws IOException {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TXDATA))
debugPrintln("Tx msg=" + msg);
// Output the FTP response
if (m_out != null) {
// Check if the response should be encrypted
if (m_sslEngine != null) {
// Encrypt the response
byte[] respByts = msg.getBytes();
ByteBuffer inByts = ByteBuffer.wrap(respByts, 0, respByts.length);
m_sslOut.position(0);
m_sslOut.limit(m_sslOut.capacity());
// Decrypt the received data
SSLEngineResult sslRes = m_sslEngine.wrap(inByts, m_sslOut);
if (m_sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
Runnable task;
while ((task = m_sslEngine.getDelegatedTask()) != null) {
task.run();
}
}
// Output the encrypted response
m_sslOut.flip();
m_sock.getOutputStream().write(m_sslOut.array(), 0, m_sslOut.remaining());
m_sock.getOutputStream().flush();
}
}
}
/**
* Process a user command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procUser(FTPRequest req)
throws IOException {
// Clear the current client information
setClientInformation(null);
setLoggedOn(false);
// Check if a user name has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error in parameters or arguments");
return;
}
// Check if secure session logons are required
if (getFTPServer().getFTPConfiguration().isFTPSEnabled() && getFTPServer().getFTPConfiguration().requireSecureSession() == true && isSecureSession() == false) {
sendFTPResponse(530, "Only secure logons are allowed, use FTPS");
return;
}
// Check for an anonymous login
if (getFTPServer().allowAnonymous() == true && req.getArgument().equalsIgnoreCase(getFTPServer().getAnonymousAccount())) {
// Anonymous login, create guest client information
ClientInfo cinfo = ClientInfo.createInfo(getFTPServer().getAnonymousAccount(), null);
cinfo.setGuest(true);
setClientInformation(cinfo);
// Return the anonymous login response
sendFTPResponse(331, "Guest login ok, send your complete e-mail address as password");
return;
}
// Create client information for the user
setClientInformation(ClientInfo.createInfo(req.getArgument(), null));
// Valid user, wait for the password
sendFTPResponse(331, "User name okay, need password for " + req.getArgument());
}
/**
* Process a password command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procPassword(FTPRequest req)
throws IOException {
// Check if the client information has been set, this indicates a user command has been
// received
if (hasClientInformation() == false) {
sendFTPResponse(500, "Syntax error, command " + req.isCommand().name() + " unrecognized");
return;
}
// Check for an anonymous login, accept any password string
if (getClientInformation().isGuest()) {
// Save the anonymous login password string
getClientInformation().setPassword(req.getArgument());
// Accept the login
setLoggedOn(true);
sendFTPResponse(230, "User logged in, proceed");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Anonymous login, info=" + req.getArgument());
}
// Validate the user
else {
// Get the client information and store the received plain text password
getClientInformation().setPassword(req.getArgument());
// Authenticate the user
FTPAuthenticator auth = getFTPServer().getFTPConfiguration().getFTPAuthenticator();
if (auth.authenticateUser(getClientInformation(), this) == true) {
// User successfully logged on
sendFTPResponse(230, "User logged in, proceed");
setLoggedOn(true);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("User " + getClientInformation().getUserName() + ", logon successful");
} else {
// Return an access denied error
sendFTPResponse(530, "Access denied");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("User " + getClientInformation().getUserName() + ", logon failed");
}
}
// If the user has successfully logged on to the FTP server then inform listeners
if (isLoggedOn())
getFTPServer().sessionLoggedOn(this);
}
/**
* Process a port command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procPort(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if the parameter has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Required argument missing");
return;
}
// Parse the address/port string into a IP address and port
StringTokenizer token = new StringTokenizer(req.getArgument(), ",");
if (token.countTokens() != 6) {
sendFTPResponse(501, "Invalid argument");
return;
}
// Parse the client address
String addrStr = token.nextToken() + "." + token.nextToken() + "." + token.nextToken() + "." + token.nextToken();
InetAddress addr = null;
try {
addr = InetAddress.getByName(addrStr);
}
catch (UnknownHostException ex) {
sendFTPResponse(501, "Invalid argument (address)");
return;
}
// Parse the client port
int port = -1;
try {
port = Integer.parseInt(token.nextToken()) * 256;
port += Integer.parseInt(token.nextToken());
}
catch (NumberFormatException ex) {
sendFTPResponse(501, "Invalid argument (port)");
return;
}
// Check if there is an existing data session
if (m_dataSess != null) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Releasing existing data session, sess=" + m_dataSess);
// Release the current data session
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// Create an active data session, the actual socket connection will be made later
m_dataSess = getFTPServer().allocateDataSession(this, addr, port);
// Return a success response to the client
sendFTPResponse(200, "Port OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Port open addr=" + addr + ", port=" + port);
}
/**
* Process a passive command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procPassive(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if there is an existing data session
if (m_dataSess != null) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Releasing existing data session, sess=" + m_dataSess);
// Release the current data session
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// Create a passive data session
try {
m_dataSess = getFTPServer().allocatePassiveDataSession(this, m_sock.getLocalAddress());
}
catch (IOException ex) {
m_dataSess = null;
}
// Check if the data session is valid
if (m_dataSess == null) {
sendFTPResponse(550, "Requested action not taken");
return;
}
// Get the passive connection address/port and return to the client
int pasvPort = m_dataSess.getPassivePort();
StringBuffer msg = new StringBuffer();
msg.append("227 Entering Passive Mode (");
msg.append(getLocalFTPAddressString());
msg.append(",");
msg.append(pasvPort >> 8);
msg.append(",");
msg.append(pasvPort & 0xFF);
msg.append(")");
sendFTPResponse(msg);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Passive open addr=" + m_sock.getLocalAddress() + ", port=" + pasvPort);
}
/**
* Process a print working directory command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procPrintWorkDir(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Return the current working directory virtual path
sendFTPResponse(257, "\"" + m_cwd.getFTPPath() + "\"");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DIRECTORY))
debugPrintln("Pwd ftp=" + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath());
}
/**
* Process a change working directory command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procChangeWorkDir(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if the request has a valid argument
if (req.hasArgument() == false) {
sendFTPResponse(501, "Path not specified");
return;
}
// Create the new working directory path
FTPPath newPath = generatePathForRequest(req, false, true);
if (newPath == null) {
sendFTPResponse(550, "Invalid path " + req.getArgument());
return;
}
// Set the new current working directory
m_cwd = newPath;
// Return a success status
sendFTPResponse(250, "Requested file action OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DIRECTORY))
debugPrintln("Cwd ftp=" + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath());
}
/**
* Process a change directory up command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procCdup(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if there is a current working directory path
if (m_cwd.isRootPath()) {
// Already at the root directory, return an error status
sendFTPResponse(550, "Already at root directory");
return;
} else {
// Remove the last directory from the path
m_cwd.removeDirectory();
if (m_cwd.isRootPath() == false && m_cwd.getSharedDevice() == null)
m_cwd.setSharedDevice(getShareList(), this);
}
// Return a success status
sendFTPResponse(250, "Requested file action OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DIRECTORY))
debugPrintln("Cdup ftp=" + m_cwd.getFTPPath() + ", share=" + m_cwd.getShareName() + ", path=" + m_cwd.getSharePath());
}
/**
* Process a long directory listing command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procList(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if the client has requested hidden files, via the '-a' option
boolean hidden = false;
if (req.hasArgument() && req.getArgument().startsWith(LIST_OPTION_PREFIX)) {
// 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
FTPPath ftpPath = m_cwd;
if (req.hasArgument())
ftpPath = generatePathForRequest(req, true, false);
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;
}
}
try {
if (ftpPath.hasSharePath() && WildCard.containsWildcards(ftpPath.getSharePath()) == false) {
// Create a temporary tree connection
TreeConnection tree = getTreeConnection(ftpPath.getSharedDevice());
// Access the virtual filesystem driver
DiskInterface disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.NotExist) {
sendFTPResponse(500, "Invalid path");
return;
}
if (sts == FileStatus.DirectoryExists) {
ftpPath.setDirectory(true);
}
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error reading file list, " + ex.toString());
}
// Always need logging - this is unexpected
debugPrintln(ex);
// Failed to send file listing
sendFTPResponse(451, "Error reading file list");
}
// 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) {
debugPrintln(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());
// Check if a path has been specified to list
List files = null;
if (req.hasArgument()) {
}
// Get a list of file information objects for the current directory
files = listFilesForPath(ftpPath, false, hidden);
// Output the file list to the client
if (files != null) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.SEARCH))
debugPrintln("List found " + files.size() + " files in " + ftpPath.getFTPPath());
// Output the file information to the client
StringBuffer str = new StringBuffer(256);
for (int i = 0; i < files.size(); i++) {
// Get the current file information
FileInfo finfo = files.get(i);
// Build the output record
str.setLength(0);
str.append(finfo.isDirectory() ? "d" : "-");
str.append("rw-rw-rw- 1 user group ");
str.append(finfo.getSize());
str.append(" ");
FTPDate.packUnixDate(str, new Date(finfo.getModifyDateTime()));
str.append(" ");
str.append(finfo.getFileName());
str.append(CRLF);
// Output the file information record
dataWrt.write(str.toString());
}
}
// End of file list transmission
sendFTPResponse(226, "Closing data connection");
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error reading file list, " + ex.toString());
}
debugPrintln(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 short directory listing command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procNList(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// 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) {
debugPrintln(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());
// Check if a path has been specified to list
List files = null;
if (req.hasArgument()) {
}
// Get a list of file information objects for the current directory
files = listFilesForPath(ftpPath, false, false);
// Output the file list to the client
if (files != null) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.SEARCH))
debugPrintln("List found " + files.size() + " files in " + ftpPath.getFTPPath());
// Output the file information to the client
for (int i = 0; i < files.size(); i++) {
// Get the current file information
FileInfo finfo = files.get(i);
// Output the file information record
dataWrt.write(finfo.getFileName());
dataWrt.write(CRLF);
}
}
// End of file list transmission
sendFTPResponse(226, "Closing data connection");
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error reading file list, " + ex.toString());
}
debugPrintln(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 system status command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procSystemStatus(FTPRequest req)
throws IOException {
// Return the system type
sendFTPResponse(215, "UNIX Type: Java FTP Server");
}
/**
* Process a server status command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procServerStatus(FTPRequest req)
throws IOException {
// Return server status information
sendFTPResponse(211, "JFileSrv - Java FTP Server");
}
/**
* Process a help command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procHelp(FTPRequest req)
throws IOException {
// Return help information
sendFTPResponse(211, "HELP text");
}
/**
* Process a no-op command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procNoop(FTPRequest req)
throws IOException {
// Return a response
sendFTPResponse(200, "");
}
/**
* Process an options request
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procOptions(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
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 (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("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 (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.SEARCH))
debugPrintln("MLst options facts=" + factStr.toString());
} else {
// Unknown options request or feature not enabled
sendFTPResponse(501, "Invalid argument");
}
}
/**
* Process a quit command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procQuit(FTPRequest req)
throws IOException {
// Return a response
sendFTPResponse(221, "Bye");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Quit closing connection(s) to client");
// Close the session(s) to the client
closeSession();
}
/**
* Process a type command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procType(FTPRequest req)
throws IOException {
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Check if ASCII or binary mode is enabled
String arg = req.getArgument().toUpperCase();
if (arg.startsWith("A"))
setBinary(false);
else if (arg.startsWith("I") || arg.startsWith("L"))
setBinary(true);
else {
// Invalid argument
sendFTPResponse(501, "Syntax error, invalid parameter");
return;
}
// Return a success status
sendFTPResponse(200, "Command OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Type arg=" + req.getArgument() + ", binary=" + m_binary);
}
/**
* Process a restart command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procRestart(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Validate the restart position
try {
m_restartPos = Integer.parseInt(req.getArgument());
}
catch (NumberFormatException ex) {
sendFTPResponse(501, "Invalid restart position");
return;
}
// Return a success status
sendFTPResponse(350, "Restart OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln("Restart pos=" + m_restartPos);
}
/**
* Process a return file command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procReturnFile(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Create the path for the file listing
FTPPath ftpPath = generatePathForRequest(req, true);
if (ftpPath == null) {
sendFTPResponse(500, "Invalid path");
return;
}
// Check if the path is the root directory
if (ftpPath.isRootPath() || ftpPath.isRootSharePath()) {
sendFTPResponse(550, "That is a directory");
return;
}
// Send the intermediate response
sendFTPResponse(150, "Connection accepted");
// 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) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error reading file list, " + ex.toString());
}
debugPrintln(ex);
}
if (dataSock == null) {
sendFTPResponse(426, "Connection closed; transfer aborted");
return;
}
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("Returning ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
// Send the file to the client
OutputStream os = null;
DiskInterface disk = null;
TreeConnection tree = null;
NetworkFile netFile = null;
try {
// Open an output stream to the client
os = dataSock.getOutputStream();
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the file exists and it is a file, if so then open the file
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
// Create the file open parameters
FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), FileAction.OpenIfExists, AccessMode.ReadOnly, 0, 0);
// Check if the file exists and it is a file
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.FileExists) {
// Open the file
netFile = disk.openFile(this, tree, params);
}
// Check if the file has been opened
if (netFile == null) {
sendFTPResponse(550, "File " + req.getArgument() + " not available");
return;
}
// Allocate the buffer for the file data
byte[] buf = new byte[DEFAULT_BUFFERSIZE];
long filePos = m_restartPos;
int len = -1;
boolean abort = false;
while (filePos < netFile.getFileSize() && abort == false) {
// Read another block of data from the file
len = disk.readFile(this, tree, netFile, buf, 0, buf.length, filePos);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln(" Write len=" + len + " bytes");
// Write the current data block to the client, update the file position
if (len > 0) {
// Write the data to the client
os.write(buf, 0, len);
// Update the file position
filePos += len;
// Check if the transfer has been aborted
abort = checkForAbort();
}
}
// Close the output stream to the client
os.close();
os = null;
// Indicate that the file has been transmitted, or the transfer was aborted
if (abort == false)
sendFTPResponse(226, "Closing data connection");
else
sendFTPResponse(426, "Transfer aborted by client");
// Close the data session
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
// Close the network file
disk.closeFile(this, tree, netFile);
netFile = null;
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln(" Transfer complete, file closed");
}
catch (SocketException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error during transfer, " + ex.toString());
debugPrintln(ex);
}
// Close the data socket to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// Indicate that there was an error during transmission of the file data
sendFTPResponse(426, "Data connection closed by client");
}
catch (FileOfflineException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Error during transfer, " + ex.toString());
// Indicate that there was an error during transmission of the file data
sendFTPResponse(450, "File data is currently offline");
}
catch (Exception ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Error during transfer, " + ex.toString());
// Indicate that there was an error during transmission of the file data
sendFTPResponse(426, "Error during transmission");
}
finally {
// Close the network file
if (netFile != null && disk != null && tree != null)
disk.closeFile(this, tree, netFile);
// Close the output stream to the client
if (os != null)
os.close();
// Close the data connection to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
}
}
/**
* Process a store file command
*
* @param req FTPRequest
* @param append boolean
* @exception IOException Socket error
*/
protected final void procStoreFile(FTPRequest req, boolean append)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Create the path for the file listing
FTPPath ftpPath = generatePathForRequest(req, true, false);
if (ftpPath == null) {
sendFTPResponse(500, "Invalid path");
return;
}
// Send the file to the client
InputStream is = null;
DiskInterface disk = null;
TreeConnection tree = null;
NetworkFile netFile = null;
FileStatus sts = FileStatus.NotExist;
// Flag to indicate if the file should be deleted on close, used if there is an error during the upload
// and the file did not exist before the upload
boolean deleteOnClose = false;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the file exists
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.DirectoryExists) {
// Return an error status
sendFTPResponse(500, "Invalid path (existing directory)");
return;
}
// Create the file open parameters
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, 0);
// Transaction begins in the innards of 'disk'
try {
// Are we opening an existing file or creating a new one?
if (sts == FileStatus.FileExists) {
// Open and truncate the existing file
netFile = disk.openFile(this, tree, params);
} else {
// Create a new file
netFile = disk.createFile(this, tree, params);
}
// Notify change listeners that a new file has been created
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
diskCtx.getChangeHandler().notifyFileChanged(NotifyAction.Added, ftpPath.getSharePath());
// 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) {
}
if (dataSock == null) {
sendFTPResponse(426, "Connection closed; transfer aborted");
return;
}
dataSock.setSoTimeout(getFTPServer().getFTPConfiguration().getFTPSrvSessionTimeout());
// Open an input stream from the client
is = dataSock.getInputStream();
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("Storing ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath() + (append ? " (Append)" : ""));
// Allocate the buffer for the file data
byte[] buf = new byte[DEFAULT_BUFFERSIZE];
long filePos = 0;
int len = is.read(buf, 0, buf.length);
boolean abort = false;
// 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();
// Read/write loop
while (len > 0 && abort == false) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln(" Receive len=" + len + " bytes");
// Write the current data block to the file, update the file position
disk.writeFile(this, tree, netFile, buf, 0, len, filePos);
filePos += len;
// Read another block of data from the client
len = is.read(buf, 0, buf.length);
// Check if the file transfer has been aborted
abort = checkForAbort();
}
// Close the input stream from the client
is.close();
is = null;
// Close the network file
disk.closeFile(this, tree, netFile);
netFile = null;
// Indicate that the file has been received, or the transfer was aborted
if (abort == false)
sendFTPResponse(226, "Closing data connection");
else
sendFTPResponse(426, "Transfer aborted by client");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln(" Transfer complete, file closed");
}
finally {
endTransaction();
}
}
catch (SocketException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Error during transfer, " + ex.toString());
// Close the data socket to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// Indicate that there was an error during transmission of the file data
sendFTPResponse(426, "Data connection closed by client");
}
catch (DiskFullException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Error during transfer, " + ex.toString());
// Close the data socket to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// If the file did not exist before the upload then mark it to delete on close
if (sts != FileStatus.FileExists) {
deleteOnClose = true;
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Marking file for delete on close (quota exceeded)");
}
// Indicate that there was an error during writing of the file
sendFTPResponse(451, "Disk full or Quota Exceeded");
}
catch (AccessDeniedException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Error during transfer, " + ex.toString());
// Close the data socket to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
// If the file did not exist before the upload then mark it to delete on close
if (sts != FileStatus.FileExists) {
deleteOnClose = true;
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln(" Marking file for delete on close (access denied)");
}
// Indicate that there was an error during writing of the file
sendFTPResponse(451, "Access denied, file may be in use or locked by another user");
}
catch (SocketTimeoutException ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error during transmission: session timeout.");
debugPrintln(" Marking file for delete on close.");
}
deleteOnClose = true;
// Indicate that there was an error during transmission of the file data
sendFTPResponse(426, "Error during transmission: session timeout");
}
catch (Exception ex) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error during transfer, " + ex.toString());
}
debugPrintln(ex);
// Indicate that there was an error during transmission of the file data
sendFTPResponse(426, "Error during transmission");
}
finally {
// Check if the file should be marked for delete on close, only when an error occurs
if (netFile != null && deleteOnClose == true)
netFile.setDeleteOnClose(true);
// Close the network file
if (netFile != null && disk != null && tree != null)
disk.closeFile(this, tree, netFile);
// Close the input stream to the client
if (is != null)
is.close();
// Close the data connection to the client
if (m_dataSess != null) {
getFTPServer().releaseDataSession(m_dataSess);
m_dataSess = null;
}
}
}
/**
* Process a delete file command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procDeleteFile(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Create the path for the file
FTPPath ftpPath = generatePathForRequest(req, true);
if (ftpPath == null) {
sendFTPResponse(550, "Invalid path specified");
return;
}
// Delete the specified file
DiskInterface disk = null;
TreeConnection tree = null;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the file exists and it is a file
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.FileExists) {
// Delete the file
disk.deleteFile(this, tree, ftpPath.getSharePath());
// Check if there are any file/directory change notify requests active
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
diskCtx.getChangeHandler().notifyFileChanged(NotifyAction.Removed, ftpPath.getSharePath());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("Deleted ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
} else {
// File does not exist or is a directory
sendFTPResponse(550, "File " + req.getArgument()
+ (sts == FileStatus.NotExist ? " not available" : " is a directory"));
return;
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error deleting file, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(450, "File action not taken");
return;
}
// Return a success status
sendFTPResponse(250, "File " + req.getArgument() + " deleted");
}
/**
* Process a rename from command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procRenameFrom(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Clear the current rename from path details, if any
m_renameFrom = null;
// Create the path for the file/directory
FTPPath ftpPath = generatePathForRequest(req, false, false);
if (ftpPath == null) {
sendFTPResponse(550, "Invalid path specified");
return;
}
// Check that the file exists, and it is a file
DiskInterface disk = null;
TreeConnection tree = null;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the file exists and it is a file
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts != FileStatus.NotExist) {
// Save the rename from file details, rename to command should follow
m_renameFrom = ftpPath;
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("RenameFrom ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
} else {
// File/directory does not exist
sendFTPResponse(550, "File " + req.getArgument()
+ (sts == FileStatus.NotExist ? " not available" : " is a directory"));
return;
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error renaming file from, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(450, "File action not taken");
return;
}
// Return a success status
sendFTPResponse(350, "File " + req.getArgument() + " OK");
}
/**
* Process a rename to command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procRenameTo(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Check if the rename from has already been set
if (m_renameFrom == null) {
sendFTPResponse(550, "Rename from not set");
return;
}
// Create the path for the new file name
FTPPath ftpPath = generatePathForRequest(req, true, false);
if (ftpPath == null) {
sendFTPResponse(550, "Invalid path specified");
return;
}
// Check that the rename is on the same share
if (m_renameFrom.getShareName().compareTo(ftpPath.getShareName()) != 0) {
sendFTPResponse(550, "Cannot rename across shares");
return;
}
// Rename the file
DiskInterface disk = null;
TreeConnection tree = null;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the file exists and it is a file
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if ((sts == FileStatus.NotExist) ||
// Special condition where we are changing case of file name but the search above is case insensitive
((sts == FileStatus.FileExists) & m_renameFrom.getSharePath().equalsIgnoreCase(ftpPath.getSharePath()))
) {
// Rename the file/directory
disk.renameFile(this, tree, m_renameFrom.getSharePath(), ftpPath.getSharePath(), null);
// Check if there are any file/directory change notify requests active
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
diskCtx.getChangeHandler().notifyRename(m_renameFrom.getSharePath(), ftpPath.getSharePath());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("RenameTo ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
} else {
// Destination file already exists or is a directory
sendFTPResponse(550, "File " + req.getArgument()
+ (sts == FileStatus.FileExists ? " already exists" : " is a directory"));
return;
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error rename to, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(450, "File action not taken");
return;
}
finally {
// Clear the rename details
m_renameFrom = null;
}
// Return a success status
sendFTPResponse(250, "File renamed OK");
}
/**
* Process a create directory command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procCreateDirectory(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Check if the new directory contains multiple directories
FTPPath ftpPath = generatePathForRequest(req, false, false);
if (ftpPath == null) {
sendFTPResponse(550, "Invalid path " + req.getArgument());
return;
}
// Create the new directory
DiskInterface disk = null;
TreeConnection tree = null;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the directory exists
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.NotExist) {
// Create the new directory
FileOpenParams params = new FileOpenParams(ftpPath.getSharePath(), FileAction.CreateNotExist,
AccessMode.ReadWrite, FileAttribute.NTDirectory, 0);
disk.createDirectory(this, tree, params);
// Notify change listeners that a new directory has been created
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
diskCtx.getChangeHandler().notifyFileChanged(NotifyAction.Added, ftpPath.getSharePath());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DIRECTORY))
debugPrintln("CreateDir ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
} else {
// File/directory already exists with that name, return an error
sendFTPResponse(450, sts == FileStatus.FileExists ? "File exists with that name" : "Directory already exists");
return;
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error creating directory, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(450, "Failed to create directory");
return;
}
// Return the FTP path to the client
sendFTPResponse(257, ftpPath.getFTPPath());
}
/**
* Process a delete directory command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procRemoveDirectory(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Check if the directory path contains multiple directories
FTPPath ftpPath = generatePathForRequest(req, false);
if (ftpPath == null) {
sendFTPResponse(550, "Invalid path " + req.getArgument());
return;
}
// 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.
if (ftpPath.isRootPath() || ftpPath.isRootSharePath()) {
sendFTPResponse(550, "Access denied, cannot delete directory in root");
return;
}
// Delete the directory
DiskInterface disk = null;
TreeConnection tree = null;
try {
// Create a temporary tree connection
tree = getTreeConnection(ftpPath.getSharedDevice());
// Check if the session has the required access to the filesystem
if (tree == null || tree.hasWriteAccess() == false) {
// Session does not have write access to the filesystem
sendFTPResponse(550, "Access denied");
return;
}
// Check if the directory exists
disk = (DiskInterface) ftpPath.getSharedDevice().getInterface();
FileStatus sts = disk.fileExists(this, tree, ftpPath.getSharePath());
if (sts == FileStatus.DirectoryExists) {
// Delete the new directory
disk.deleteDirectory(this, tree, ftpPath.getSharePath());
// Check if there are any file/directory change notify requests active
DiskDeviceContext diskCtx = (DiskDeviceContext) tree.getContext();
if (diskCtx.hasChangeHandler())
diskCtx.getChangeHandler().notifyFileChanged(NotifyAction.Removed, ftpPath.getSharePath());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DIRECTORY))
debugPrintln("DeleteDir ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", path="
+ ftpPath.getSharePath());
} else {
// File already exists with that name or directory does not exist return an error
sendFTPResponse(550, sts == FileStatus.FileExists ? "File exists with that name" : "Directory does not exist");
return;
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error deleting directory, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(550, "Failed to delete directory");
return;
}
// Return a success status
sendFTPResponse(250, "Directory deleted OK");
}
/**
* Process a machine listing request, single folder
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procMachineListing(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPResponse(530, "Not logged in");
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// 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 (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("Mlst ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", info=" + finfo);
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error retrieving file information, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(550, "Error retrieving file information");
}
}
/**
* Process a machine listing request, folder contents
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procMachineListingContents(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
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) {
debugPrintln(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
List files = null;
files = listFilesForPath(ftpPath, false, false);
// Output the file list to the client
if (files != null) {
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.SEARCH))
debugPrintln("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 = files.get(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
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error reading file list, " + ex.toString());
}
debugPrintln(ex);
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 get modification date/time command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procGetModifyDateTime(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Create the path for the file request
FTPPath ftpPath = generatePathForRequest(req, true);
if (ftpPath == null) {
sendFTPResponse(550, "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, "File " + req.getArgument() + " not available");
return;
}
// Return the file modification date/time
if (finfo.hasModifyDateTime())
sendFTPResponse(213, FTPDate.packMlstDateTime(finfo.getModifyDateTime()));
else
sendFTPResponse(550, "Modification date/time not available for " + finfo.getFileName());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("File modify date/time ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName()
+ ", modified=" + finfo.getModifyDateTime());
}
catch (Exception ex) {
sendFTPResponse(550, "Error retrieving file modification date/time");
}
}
/**
* Process a modify date/time command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procModifyDateTime(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
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
String path = req.getArgument();
long modifyDateTime = 0L;
if (path.length() > MDTM_DATETIME_MINLEN && path.indexOf(' ') != -1) {
// Check if the first argument looks like a date/time value
boolean settime = true;
for (int i = 0; i < MDTM_DATETIME_MINLEN; i++) {
if (Character.isDigit(path.charAt(i)) == false)
settime = false;
}
// Looks like a date/time value
if (settime == true) {
try {
// Parse the various fields
int year = Integer.valueOf(path.substring(0, 4)).intValue();
int month = Integer.valueOf(path.substring(4, 6)).intValue();
int day = Integer.valueOf(path.substring(6, 8)).intValue();
int hours = Integer.valueOf(path.substring(8, 10)).intValue();
int mins = Integer.valueOf(path.substring(10, 12)).intValue();
int secs = Integer.valueOf(path.substring(12, 14)).intValue();
// Check if the date/time includes milliseconds
int millis = 0;
int sep = path.indexOf(' ', MDTM_DATETIME_MINLEN);
if (path.charAt(MDTM_DATETIME_MINLEN) == '.') {
// Find the seperator between the date/time and path
millis = Integer.valueOf(path.substring(MDTM_DATETIME_MINLEN + 1, sep)).intValue();
}
// Create the modify date/time, month is zero based
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.set(year, month - 1, day, hours, mins, secs);
if (millis != 0)
cal.set(Calendar.MILLISECOND, millis);
// Get the modify date/time
modifyDateTime = cal.getTimeInMillis();
// Remove the date/time from the request argument
path = path.substring(sep + 1);
req.updateArgument(path);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("Modify date/time arg=" + path + ", utcTime=" + modifyDateTime);
}
catch (NumberFormatException ex) {
}
}
}
// Create the path for the file request
FTPPath ftpPath = generatePathForRequest(req, true);
if (ftpPath == null) {
sendFTPResponse(550, "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();
// Check if the modify date/time should be set
if (modifyDateTime != 0L) {
// Set the file/folder modification date/time
FileInfo finfo = new FileInfo();
finfo.setModifyDateTime(modifyDateTime);
finfo.setFileInformationFlags(FileInfo.SetModifyDate);
disk.setFileInformation(this, tree, ftpPath.getSharePath(), finfo);
}
// Get the file information
FileInfo finfo = disk.getFileInformation(this, tree, ftpPath.getSharePath());
if (finfo == null) {
sendFTPResponse(550, "File " + req.getArgument() + " not available");
return;
}
// Return the file modification date/time
if (finfo.hasModifyDateTime())
sendFTPResponse(213, FTPDate.packMlstDateTime(finfo.getModifyDateTime()));
else
sendFTPResponse(550, "Modification date/time not available for " + finfo.getFileName());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("File modify date/time ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName()
+ ", modified=" + finfo.getModifyDateTime());
}
catch (Exception ex) {
sendFTPResponse(550, "Error retrieving file modification date/time");
}
}
/**
* Process a server features request
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procFeatures(FTPRequest req)
throws IOException {
// Return the list of supported server features
sendFTPResponse("211-Features supported");
// Modify date/time and size commands supported
if (FeatureMFMT)
sendFTPResponse(" MFMT");
if (FeatureSIZE)
sendFTPResponse(" SIZE");
if (FeatureUTF8)
sendFTPResponse(" UTF8");
// Machine listing supported, build the fact list
if (FeatureMLST) {
StringBuffer mlstStr = new StringBuffer();
mlstStr.append(" MLST ");
for (int i = 0; i < _factNames.length; i++) {
// Output the fact name
mlstStr.append(_factNames[i]);
// Check if the fact is enabled by default
if ((MLST_DEFAULT & (1 << i)) != 0)
mlstStr.append("*");
mlstStr.append(";");
}
sendFTPResponse(mlstStr.toString());
sendFTPResponse(" MLSD");
}
if (FeatureAUTH)
sendFTPResponse(" AUTH TLS");
sendFTPResponse(211, "END");
}
/**
* Process a file size command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procFileSize(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if an argument has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Syntax error, parameter required");
return;
}
// Create the path for the file listing
FTPPath ftpPath = generatePathForRequest(req, true);
if (ftpPath == null) {
sendFTPResponse(550, "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, "File " + req.getArgument() + " not available");
return;
}
// Return the file size
sendFTPResponse(213, "" + finfo.getSize());
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.FILE))
debugPrintln("File size ftp=" + ftpPath.getFTPPath() + ", share=" + ftpPath.getShareName() + ", size="
+ finfo.getSize());
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Error retriving file size, " + ex.toString());
}
debugPrintln(ex);
sendFTPResponse(550, "Error retrieving file size");
}
}
/**
* Process a site specific command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procSite(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if the FTP server has a site interface
if (getFTPServer().hasSiteInterface()) {
// Pass the request to the site interface
FTPSiteInterface siteInterface = getFTPServer().getSiteInterface();
siteInterface.processFTPSiteCommand(this, req);
} else {
// SITE command not implemented
sendFTPResponse(501, "SITE commands not implemented");
}
}
/**
* Process a structure command. This command is obsolete.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procStructure(FTPRequest req)
throws IOException {
// Check for the file structure argument
if (req.hasArgument() && req.getArgument().equalsIgnoreCase("F")) {
// Return a success status
sendFTPResponse(200, "OK");
} else {
// Return an error response
sendFTPResponse(504, "Obsolete");
}
}
/**
* Process a mode command. This command is obsolete.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procMode(FTPRequest req)
throws IOException {
// Check for the stream transfer mode argument
if (req.hasArgument() && req.getArgument().equalsIgnoreCase("S")) {
// Return a success status
sendFTPResponse(200, "OK");
} else {
// Return an error response
sendFTPResponse(504, "Obsolete");
}
}
/**
* Process an allocate command. This command is obsolete.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procAllocate(FTPRequest req)
throws IOException {
// Return a response
sendFTPResponse(202, "Obsolete");
}
/**
* Process an abort command. The main abort processing is done in the store/return file handling
* during an active file transfer.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procAbort(FTPRequest req)
throws IOException {
// Return a success response
sendFTPResponse(226, "No active transfer to abort");
}
/**
* Process an authentication command for SSL/TLS.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procAuth(FTPRequest req)
throws IOException {
// Check if SSL/TLS sessions are enabled
if (getFTPServer().getFTPConfiguration().isFTPSEnabled() == false) {
sendFTPResponse(534, "SSL/TLS sessions not available");
return;
}
// Switch the control session into SSL/TLS mode
try {
// Check for SSL or TLS type
if (req.hasArgument()) {
String engineTyp = req.getArgument().toUpperCase();
if (engineTyp.equals("SSL") || engineTyp.equals("TLS")) {
// Initialize the SSL engine
setupSSLEngine(engineTyp);
// Send a response to indicate the socket is ready to switch to SSL/TLS mode
sendUnencryptedFTPResponse(234, "Switching to " + engineTyp + " secure session");
}
} else {
// Type not specified
sendFTPResponse(421, "Failed to negotiate SSL/TLS, type not specified");
}
}
catch (Exception ex) {
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.ERROR)) {
debugPrintln(" Faile to negotiate SSL/TLS, " + ex.toString());
}
debugPrintln(ex);
m_sslEngine = null;
sendFTPResponse(421, "Failed to negotiate SSL/TLS");
}
}
/**
* Process a protected buffer size command.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procProtectedBufferSize(FTPRequest req)
throws IOException {
// Check if the control session is using SSL/TLS
if (m_sslEngine == null) {
sendFTPResponse(503, "Not using secure connection");
return;
}
// Convert the buffer size argument
String arg = req.getArgument();
if (arg == null || arg.length() == 0) {
sendFTPResponse(501, "Empty buffer size argument");
return;
}
// Parse the buffer size argument
try {
m_pbSize = Integer.parseInt(arg);
}
catch (NumberFormatException ex) {
sendFTPResponse(501, "Invalid buffer size argument");
return;
}
// Return a success status
sendFTPResponse(200, "Buffer size ok");
}
/**
* Process a data channel protection level command.
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procDataChannelProtection(FTPRequest req)
throws IOException {
// Check that the protected buffer size has been negotiated
if (m_pbSize == -1) {
sendFTPResponse(503, "Protected buffer size not negotiated");
return;
}
// Validate the protection level
String arg = req.getArgument().toUpperCase();
if (arg == null || ProtLevels.indexOf(arg) == -1) {
sendFTPResponse(504, "Invalid protection level, " + arg);
return;
}
// Only accept the 'clear' protection level
if (arg.equals(ProtLevelClear)) {
// Accept the clear protection level, data connections sent in clear text
sendFTPResponse(200, "Protection level accepted");
} else {
// Reject the protection level for now, we do not support protected data connections
sendFTPResponse(534, "Protected data connections not supported");
}
}
/**
* Process a clear command channel command
*
* @param ftpReq FTPRequest
* @exception IOException Socket error
*/
protected final void procClearCommandChannel(FTPRequest ftpReq)
throws IOException {
// Check if the control session is using SSL/TLS
if (m_sslEngine == null) {
sendFTPResponse(533, "Not using secure connection");
return;
}
// Send the response over the protected session
sendFTPResponse(200, "Secure connection closed");
// Close the SSL engine
m_sslEngine.closeOutbound();
getSSLCommand(m_inbuf, 0);
// Release resources used by the secure connection
m_sslEngine = null;
m_sslContext = null;
m_sslIn = null;
m_sslOut = null;
}
/**
* Process an extended port command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procExtendedPort(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Check if the parameter has been specified
if (req.hasArgument() == false) {
sendFTPResponse(501, "Required argument missing");
return;
}
// Parse the client address
InetSocketAddress clientAddr;
try {
clientAddr = parseExtendedAddress(req.getArgument());
}
catch (Exception ex) {
sendFTPResponse(501, ex.getMessage());
return;
}
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Opening data socket addr=" + clientAddr.getAddress() + ", port=" + clientAddr.getPort());
// Create an active data session, the actual socket connection will be made later
m_dataSess = getFTPServer().allocateDataSession(this, clientAddr.getAddress(), clientAddr.getPort());
// Return a success response to the client
sendFTPResponse(200, "Port OK");
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Extended port open addr=" + clientAddr.getAddress() + ", port=" + clientAddr.getPort());
}
/**
* Process an extended passive command
*
* @param req FTPRequest
* @exception IOException Socket error
*/
protected final void procExtendedPassive(FTPRequest req)
throws IOException {
// Check if the user is logged in
if (isLoggedOn() == false) {
sendFTPNotLoggedOnResponse();
return;
}
// Create a passive data session
try {
m_dataSess = getFTPServer().allocatePassiveDataSession(this, m_sock.getLocalAddress());
}
catch (IOException ex) {
m_dataSess = null;
}
// Check if the data session is valid
if (m_dataSess == null) {
sendFTPResponse(550, "Requested action not taken");
return;
}
// Get the passive connection address/port and return to the client
int pasvPort = m_dataSess.getPassivePort();
StringBuffer msg = new StringBuffer();
msg.append("229 Entering Extended Passive Mode (|||");
msg.append(pasvPort);
msg.append("|)");
sendFTPResponse(msg);
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("Extended passive open addr=" + m_sock.getLocalAddress() + ", port=" + pasvPort);
}
/**
* Parse the extended network address string
*
* Comments for ALF-684
* RFC 2428:
*
* AF Number Protocol
* --------- --------
* 1 Internet Protocol, Version 4 [Pos81a]
* 2 Internet Protocol, Version 6 [DH96]
*
* AF Number Address Format Example
* --------- -------------- -------
* 1 dotted decimal 132.235.1.2
* 2 IPv6 string 1080::8:800:200C:417A
* representations
* defined in [HD96]
*
* The following are sample EPRT commands:
*
* EPRT |1|132.235.1.2|6275|
* EPRT |2|1080::8:800:200C:417A|5282|
*
* So, we need to parse the InternetAddress according to AF Number.
* That means we don't need to do any 'instanceof', just check the address
* validity and raise the error, e.g. 501 (IP Address is not valid) if the
* address is not convenient to AF Number.
*
* @param extAddr String
* @return InetSocketAddress
*/
private final InetSocketAddress parseExtendedAddress(String extAddr) {
// Make sure the string is valid
if (extAddr == null || extAddr.length() < 7)
throw new IllegalArgumentException("Invalid argument");
// Split the string into network type, network address and port strings
StringTokenizer tokens = new StringTokenizer(extAddr, extAddr.substring(0, 1));
if (tokens.countTokens() < 3)
throw new IllegalArgumentException("Invalid argument");
String netType = tokens.nextToken();
String netAddr = tokens.nextToken();
String netPort = tokens.nextToken();
int afNumber = 0;
try {
afNumber = Integer.parseInt(netType);
}
catch (NumberFormatException ex) {
}
if (afNumber == TypeIPv4 || afNumber == TypeIPv6) {
InetSocketAddress sockAddr = null;
// Since Java handles IPv4/IPv6 addresses transparently as it is saying in
// http://download.oracle.com/javase/1.5.0/docs/guide/net/ipv6_guide/index.html
// We don't care about AF Number and Address Format, but just checking a validity
// of the IP.
InetAddress addr = null;
try {
addr = InetAddress.getByName(netAddr);
}
catch (UnknownHostException ex) {
}
InetAddress remoteAddr = m_sock.getInetAddress();
if (!addr.equals(remoteAddr)) {
if (Debug.EnableWarn && hasDebug(FTPSrvSession.Dbg.DATAPORT))
debugPrintln("EPRT address [" + addr + "] is not equal to client address [" + remoteAddr + "]. For security purposes client address is used for data transmission.");
addr = remoteAddr;
}
// Avoid route connection problems with addresses where an interface is specified.
// I.e. fe80:0:0:0:a00:27ff:fe42:94f7%eth1
if (addr != null && addr instanceof Inet6Address) {
try {
addr = InetAddress.getByAddress(addr.getAddress());
}
catch (UnknownHostException ex) {
throw new IllegalArgumentException("Unknown host");
}
}
int port = -1;
try {
port = Integer.parseInt(netPort);
}
catch (NumberFormatException ex) {
}
if (port != -1)
sockAddr = new InetSocketAddress(addr, port);
return sockAddr;
}
throw new IllegalArgumentException("Invalid address/port argument");
}
/**
* Build a list of file name or file information objects for the specified server path
*
* @param path FTPPath
* @param nameOnly boolean
* @param hidden boolean
* @return List of file information objects
*/
protected final List listFilesForPath(FTPPath path, boolean nameOnly, boolean hidden) {
// Check if the path is valid
if (path == null)
return null;
// Check if the path is the root path
List files = new ArrayList();
if (path.hasSharedDevice() == false) {
// The first level of directories are mapped to the available shares
SharedDeviceList shares = getShareList();
if (shares != null) {
// Search for disk shares
Enumeration enm = shares.enumerateShares();
while (enm.hasMoreElements()) {
// Get the current shared device
SharedDevice shr = enm.nextElement();
// Create a file information object for the top level directory details
files.add(new FileInfo(shr.getName(), 0L, FileAttribute.Directory));
}
}
} else {
// Append a wildcard to the search path
String searchPath = path.getSharePath();
if (path.isDirectory())
searchPath = path.makeSharePathToFile("*.*");
// Create a temporary tree connection
TreeConnection tree = new TreeConnection(path.getSharedDevice());
// Start a search on the specified disk share
DiskInterface disk = null;
SearchContext ctx = null;
int searchAttr = FileAttribute.Directory + FileAttribute.Normal;
if (hidden)
searchAttr += FileAttribute.Hidden;
try {
disk = (DiskInterface) path.getSharedDevice().getInterface();
ctx = disk.startSearch(this, tree, searchPath, searchAttr, EnumSet.noneOf( SearchFlags.class));
}
catch (Exception ex) {
}
// Add the files to the list
if (ctx != null) {
// Get the file names/information
while (ctx.hasMoreFiles()) {
// Check if a file name or file information is required
if (nameOnly) {
// Add a file name to the list
files.add( new FileInfo( ctx.nextFileName(), 0, FileAttribute.NTNormal));
} else {
// Create a file information object
FileInfo finfo = new FileInfo();
if (ctx.nextFileInfo(finfo) == false)
break;
if (finfo.getFileName() != null)
files.add(finfo);
}
}
}
}
// Return the list of file names/information
return files;
}
/**
* Get the list of filtered shares that are available to this session
*
* @return SharedDeviceList
*/
protected final SharedDeviceList getShareList() {
// Check if the filtered share list has been initialized
if (m_shares == null) {
// Get a list of shared filesystems
SharedDeviceList shares = getFTPServer().getShareMapper().getShareList(getFTPServer().getServerName(), this, false);
// Search for disk shares
m_shares = new SharedDeviceList();
Enumeration enm = shares.enumerateShares();
while (enm.hasMoreElements()) {
// Get the current shared device
SharedDevice shr = enm.nextElement();
// Check if the share is a disk share
if (shr instanceof DiskSharedDevice)
m_shares.addShare(shr);
}
// Check if there is an access control manager available, if so then filter the list of
// shared filesystems
if (getServer().hasAccessControlManager()) {
// Get the access control manager
AccessControlManager aclMgr = getServer().getAccessControlManager();
// Filter the list of shared filesystems
m_shares = aclMgr.filterShareList(this, m_shares);
}
}
// Return the filtered shared filesystem list
return m_shares;
}
/**
* Get a tree connection for the specified shared device. Creates and caches a new tree
* connection if required.
*
* @param share SharedDevice
* @return TreeConnection
*/
protected final TreeConnection getTreeConnection(SharedDevice share) {
// Check if the share is valid
if (share == null)
return null;
// Check if there is a tree connection in the cache
TreeConnection tree = m_connections.findConnection(share.getName());
if (tree == null) {
// Create a new tree connection
tree = new TreeConnection(share);
m_connections.addConnection(tree);
// Set the access permission for the shared filesystem
if (getServer().hasAccessControlManager()) {
// Set the access permission to the shared filesystem
AccessControlManager aclMgr = getServer().getAccessControlManager();
int access = aclMgr.checkAccessControl(this, share);
if (access != AccessControl.Default)
tree.setPermission(access);
} else {
// Allow full access to the filesystem
tree.setPermission(ISMBAuthenticator.ShareStatus.WRITEABLE);
}
}
// Return the connection
return tree;
}
/**
* Check if an abort command has been sent by the client
*
* @return boolean
*/
private final boolean checkForAbort() {
try {
// Check if there is any pending data on the command socket
if (m_in.available() > 0) {
// Read the next request
FTPRequest ftpReq = getNextCommand(false);
if (ftpReq != null) {
// Check for an abort command
if (ftpReq.isCommand() == FTPCommand.ABOR) {
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.FILEIO))
debugPrintln("Transfer aborted by client");
// Indicate an abort has been received
return true;
} else {
// Queue the request for processing later
m_ftpCmdList.add(ftpReq);
}
}
}
}
catch (IOException ex) {
// DEBUG
if (Debug.EnableError && hasDebug(FTPSrvSession.Dbg.ERROR))
debugPrintln("Error during check for abort, " + ex.toString());
}
// No command, or not an abort command
return false;
}
/**
* Read the next FTP command from the command socket, or get a command from the list of queued
* requests
*
* @param checkQueue boolean
* @return FTPRequest
* @exception SocketException Socket error
* @exception IOException Socket error
*/
private final FTPRequest getNextCommand(boolean checkQueue)
throws SocketException, IOException {
// Check if there are any queued requests
FTPRequest nextReq = null;
if (checkQueue == true && m_ftpCmdList.size() > 0) {
// Get the next queued request
nextReq = m_ftpCmdList.remove(0);
} else {
// Loop until a valid request is received, or the connection is closed
while (nextReq == null) {
// Wait for an incoming request
int rdlen = m_in.read(m_inbuf);
// Check if there is no more data, the other side has dropped the connection
if (rdlen == -1) {
closeSession();
return null;
} else if (rdlen == m_inbuf.length) {
// Looks like there is more data to be read for the current command, we need to extend the buffer
//
// Check if the command buffer has already been extended to the maximum size
if (m_inbuf.length < MaxCommandBufSize) {
// Extend the command buffer
int curLen = m_inbuf.length;
int availLen = m_in.available();
int newLen = Math.max(m_inbuf.length * 2, curLen + availLen + 50);
if (newLen > MaxCommandBufSize)
newLen = MaxCommandBufSize;
// Check if the new buffer size is large enough for the current command
if (newLen > (curLen + availLen)) {
// Allocate a new buffer and copy the existing data over to it
byte[] newbuf = new byte[newLen];
System.arraycopy(m_inbuf, 0, newbuf, 0, m_inbuf.length);
// Move the new command buffer into place
m_inbuf = newbuf;
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.RXDATA))
debugPrintln("Extended command buffer to " + m_inbuf.length + " bytes");
// Read the remaining data
int rdlen2 = m_in.read(m_inbuf, curLen, m_inbuf.length - curLen);
if (rdlen2 == -1) {
closeSession();
return null;
} else {
// Calculate the total read length
rdlen = rdlen + rdlen2;
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.RXDATA))
debugPrintln("Secondary read " + rdlen2 + " bytes, total bytes read " + rdlen);
}
}
} else {
// Command is too large, clear any pending data on the command socket and ignore it
clearCommandSocket();
// DEBUG
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.RXDATA))
debugPrintln("Received command too large, ignored");
return null;
}
}
// If there is an SSL engine associated with this session then decrypt the received data
if (m_sslEngine != null)
rdlen = getSSLCommand(m_inbuf, rdlen);
// Trim the trailing
if (rdlen > 0) {
while (rdlen > 0 && m_inbuf[rdlen - 1] == '\r' || m_inbuf[rdlen - 1] == '\n')
rdlen--;
// Get the command string, create the new request
String cmd = null;
if (isUTF8Enabled()) {
cmd = m_normalizer.normalize(new String(m_inbuf, 0, rdlen, "UTF8"));
} else
cmd = new String(m_inbuf, 0, rdlen);
nextReq = new FTPRequest(cmd);
}
}
}
// Return the request
return nextReq;
}
/**
* Clear the command socket of pending data
*
* @exception IOException Socket error
*/
protected void clearCommandSocket()
throws IOException {
// Loop until all data has been cleared from the command socket or the socket is closed
int rdlen = 0;
while (m_in.available() > 0 && rdlen >= 0) {
// Read a block of data from the command socket
rdlen = m_in.read(m_inbuf);
if (rdlen == -1)
closeSession();
}
}
/**
* Get the next command data on an SSL/TLS encrypted connection
*
* @param buf byte[]
* @param len int
* @return int
* @exception SocketException Socket error
* @exception IOException Socket error
*/
protected final int getSSLCommand(byte[] buf, int len)
throws SocketException, IOException {
m_sslIn.limit(m_sslIn.capacity());
m_sslIn.position(len);
m_sslIn.flip();
m_sslOut.clear();
// Get the SSL engine status
SSLEngineResult sslRes = m_sslEngine.unwrap(m_sslIn, m_sslOut);
while (m_sslIn.position() < len) {
sslRes = m_sslEngine.unwrap(m_sslIn, m_sslOut);
}
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL unwrap() len=" + len + ", returned " + sslRes.bytesProduced() + " bytes, res=" + sslRes + ", sts=" + sslRes.getStatus());
int unwrapLen = sslRes.bytesProduced();
boolean loopDone = false;
Runnable task = null;
while ( !loopDone && m_sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING &&
sslRes.getStatus() != SSLEngineResult.Status.CLOSED) {
switch (m_sslEngine.getHandshakeStatus()) {
case NEED_TASK:
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL engine status=NEED_TASK");
// Run the SSL engine task in the current thread
while ((task = m_sslEngine.getDelegatedTask()) != null) {
task.run();
}
break;
case NEED_WRAP:
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL engine status=NEED_WRAP");
m_sslIn.limit(m_sslIn.capacity());
m_sslIn.flip();
m_sslOut.clear();
while (m_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
sslRes = m_sslEngine.wrap(m_sslIn, m_sslOut);
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" wrap() returned " + sslRes.bytesProduced() + " bytes, res=" + sslRes + ", sts=" + sslRes.getStatus());
if ( sslRes.bytesProduced() == 0)
break;
}
// Send the output to the client
m_sslOut.flip();
if (m_sslOut.remaining() > 0) {
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" Send data to client = " + m_sslOut.remaining());
// Send the encrypted data to the client
m_sock.getOutputStream().write(m_sslOut.array(), 0, m_sslOut.remaining());
m_sock.getOutputStream().flush();
}
break;
case NEED_UNWRAP:
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL engine status=NEED_UNWRAP");
// Read more data from the socket
int rdlen = m_in.read(m_inbuf);
// Check if there is no more data, the other side has dropped the connection
if (rdlen == -1) {
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" Socket read returned -1, closing session");
// Close the FTP session
closeSession();
return 0;
}
m_sslIn.limit(m_sslIn.capacity());
m_sslIn.position(rdlen);
m_sslIn.flip();
while (m_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP && m_sslIn.remaining() > 0) {
m_sslOut.limit(m_sslOut.capacity());
sslRes = m_sslEngine.unwrap(m_sslIn, m_sslOut);
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" unwrap() len=" + rdlen + ",returned " + sslRes.bytesProduced() + " bytes, res=" + sslRes + ", sts=" + sslRes.getStatus());
// Run the SSL engine task in the current thread
if (m_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
while ((task = m_sslEngine.getDelegatedTask()) != null) {
task.run();
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" task during unwrap");
}
}
}
m_sslOut.flip();
if (m_sslOut.remaining() > 0) {
if (m_sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
unwrapLen = m_sslOut.remaining();
loopDone = true;
} else {
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln(" Send data to client = " + m_sslOut.remaining());
// Send the encrypted data to the client
m_sock.getOutputStream().write(m_sslOut.array(), 0, m_sslOut.remaining());
m_sock.getOutputStream().flush();
}
}
break;
case NOT_HANDSHAKING:
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL engine status=NOT_HANDSHAKING");
loopDone = true;
break;
case FINISHED:
// DEBUG
if (Debug.EnableDbg && hasDebug(FTPSrvSession.Dbg.SSL))
debugPrintln("SSL engine status=FINISHED");
loopDone = true;
break;
}
}
// Move decrypted data to the input buffer
if (unwrapLen > 0)
System.arraycopy(m_sslOut.array(), 0, m_inbuf, 0, unwrapLen);
// Return the decrypted data length
return unwrapLen;
}
/**
* Initialize the SSL engine when SSL mode is enabled on the command socket
*
* @param engineTyp String
* @exception IOException Socket error
* @throws NoSuchAlgorithmException No such SSL algorithm error
* @throws CertificateException Certificate error
* @throws KeyStoreException Key store error
* @throws UnrecoverableKeyException Unrecoverable key error
* @throws KeyManagementException Key management error
*/
protected final void setupSSLEngine(String engineTyp)
throws IOException, NoSuchAlgorithmException, CertificateException,
KeyStoreException, UnrecoverableKeyException, KeyManagementException {
// Get the FTP configuration
FTPConfigSection ftpConfig = getFTPServer().getFTPConfiguration();
// Load the key store and trust store
KeyStore keyStore = KeyStore.getInstance(ftpConfig.getKeyStoreType());
keyStore.load(new FileInputStream(ftpConfig.getKeyStorePath()), ftpConfig.getKeyStorePassphrase());
String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(defaultAlgorithm);
keyFactory.init(keyStore, ftpConfig.getKeyStorePassphrase());
defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
m_sslContext = SSLContext.getInstance(engineTyp);
// MNT-7301 FTPS server requires unnecessarily to have a trustStore while a keyStore should be sufficient
TrustManager[] trManager = null;
if (ftpConfig.getTrustStorePath() != null) {
KeyStore trustStore = KeyStore.getInstance(ftpConfig.getTrustStoreType());
trustStore.load(new FileInputStream(ftpConfig.getTrustStorePath()), ftpConfig.getTrustStorePassphrase());
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
trustFactory.init(trustStore);
trManager = trustFactory.getTrustManagers();
}
m_sslContext.init(keyFactory.getKeyManagers(), trManager, null);
m_sslEngine = m_sslContext.createSSLEngine();
m_sslEngine.setUseClientMode(false);
m_sslEngine.setWantClientAuth(true);
m_sslEngine.beginHandshake();
SSLSession sslSess = m_sslEngine.getSession();
m_sslOut = ByteBuffer.allocate(sslSess.getApplicationBufferSize() + 50);
if (m_inbuf.length < sslSess.getApplicationBufferSize())
m_inbuf = new byte[sslSess.getApplicationBufferSize()];
m_sslIn = ByteBuffer.wrap(m_inbuf);
}
/**
* Check if the session is in SSL/TLS mode
*
* @return boolean
*/
protected final boolean isSecureSession() {
return m_sslEngine != null ? true : false;
}
/**
* Generate a machine listing string for the specified file/folder information
*
* @param finfo FileInfo
* @param mlstFlags int
* @param buf StringBuffer
* @param isMlsd boolean
*/
protected final void generateMlstString(FileInfo finfo, int mlstFlags, StringBuffer buf, boolean isMlsd) {
// Create the machine listing record
for (int i = 0; i < _factNames.length; i++) {
// Check if the current fact is enabled
int curFact = 1 << i;
if ((mlstFlags & curFact) != 0) {
// Output the fact value
switch (curFact) {
// File size
case MLST_SIZE:
buf.append(_factNames[i]);
buf.append("=");
buf.append(finfo.getSize());
buf.append(";");
break;
// Modify date/time
case MLST_MODIFY:
if (finfo.hasModifyDateTime()) {
buf.append(_factNames[i]);
buf.append("=");
buf.append(FTPDate.packMlstDateTime(finfo.getModifyDateTime()));
buf.append(";");
}
break;
// Creation date/time
case MLST_CREATE:
if (finfo.hasCreationDateTime()) {
buf.append(_factNames[i]);
buf.append("=");
buf.append(FTPDate.packMlstDateTime(finfo.getCreationDateTime()));
buf.append(";");
}
break;
// Type
case MLST_TYPE:
buf.append(_factNames[i]);
if (finfo.isDirectory() == false) {
buf.append("=file;");
} else {
buf.append("=dir;");
}
break;
// Unique identifier
case MLST_UNIQUE:
if (finfo.getFileId() != -1) {
buf.append(_factNames[i]);
buf.append("=");
buf.append(finfo.getFileId());
buf.append(";");
}
break;
// Permissions
case MLST_PERM:
buf.append(_factNames[i]);
buf.append("=");
if (finfo.isDirectory()) {
buf.append(finfo.isReadOnly() ? "el" : "ceflmp");
} else {
buf.append(finfo.isReadOnly() ? "r" : "rwadf");
}
buf.append(";");
break;
// Media-type
case MLST_MEDIATYPE:
break;
}
}
}
// Add the file name
buf.append(" ");
buf.append(finfo.getFileName());
}
/**
* Return the local IP address as a string in 'n,n,n,n' format
*
* @return String
*/
private final String getLocalFTPAddressString() {
return m_sock.getLocalAddress().getHostAddress().replace('.', ',');
}
/**
* Return a not logged status
*
* @exception IOException Socket error
*/
protected final void sendFTPNotLoggedOnResponse()
throws IOException {
sendFTPResponse(530, "Not logged on");
}
/**
* Indicate that FTP filesystem searches are case sensitive
*
* @return boolean
*/
public boolean useCaseSensitiveSearch() {
return true;
}
/**
* Start the FTP session in a seperate thread
*/
public void run() {
try {
// Debug
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("FTP session started");
// Create the input/output streams
m_in = m_sock.getInputStream();
m_out = new OutputStreamWriter(m_sock.getOutputStream());
m_inbuf = new byte[DefCommandBufSize];
// Return the initial response
sendFTPResponse(220, "FTP server ready");
// Start/end times if timing debug is enabled
long startTime = 0L;
long endTime = 0L;
// The server session loops until the NetBIOS hangup state is set.
FTPRequest ftpReq = null;
while (m_sock != null) {
// Wait for a request
ftpReq = getNextCommand(true);
if (ftpReq == null)
continue;
// Debug
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TIMING))
startTime = System.currentTimeMillis();
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.RXDATA))
debugPrintln("Rx cmd=" + ftpReq);
// Parse the received command, and validate
switch (ftpReq.isCommand()) {
// User command
case USER:
procUser(ftpReq);
break;
// Password command
case PASS:
procPassword(ftpReq);
break;
// Quit command
case QUIT:
procQuit(ftpReq);
break;
// Type command
case TYPE:
procType(ftpReq);
break;
// Port command
case PORT:
procPort(ftpReq);
break;
// Passive command
case PASV:
procPassive(ftpReq);
break;
// Restart position command
case REST:
procRestart(ftpReq);
break;
// Return file command
case RETR:
procReturnFile(ftpReq);
// Reset the restart position
m_restartPos = 0;
break;
// Store file command
case STOR:
procStoreFile(ftpReq, false);
break;
// Append file command
case APPE:
procStoreFile(ftpReq, true);
break;
// Print working directory command
case PWD:
case XPWD:
procPrintWorkDir(ftpReq);
break;
// Change working directory command
case CWD:
case XCWD:
procChangeWorkDir(ftpReq);
break;
// Change to previous directory command
case CDUP:
case XCUP:
procCdup(ftpReq);
break;
// Full directory listing command
case LIST:
procList(ftpReq);
break;
// Short directory listing command
case NLST:
procNList(ftpReq);
break;
// Delete file command
case DELE:
procDeleteFile(ftpReq);
break;
// Rename file from command
case RNFR:
procRenameFrom(ftpReq);
break;
// Rename file to comand
case RNTO:
procRenameTo(ftpReq);
break;
// Create new directory command
case MKD:
case XMKD:
procCreateDirectory(ftpReq);
break;
// Delete directory command
case RMD:
case XRMD:
procRemoveDirectory(ftpReq);
break;
// Return file size command
case SIZE:
procFileSize(ftpReq);
break;
// Return the modification date/time
case MDTM:
procGetModifyDateTime(ftpReq);
break;
// Set modify date/time command
case MFMT:
procModifyDateTime(ftpReq);
break;
// System status command
case SYST:
procSystemStatus(ftpReq);
break;
// Server status command
case STAT:
procServerStatus(ftpReq);
break;
// Help command
case HELP:
procHelp(ftpReq);
break;
// No-op command
case NOOP:
procNoop(ftpReq);
break;
// Abort command
case ABOR:
procAbort(ftpReq);
break;
// Server features command
case FEAT:
procFeatures(ftpReq);
break;
// Options command
case OPTS:
procOptions(ftpReq);
break;
// Machine listing, single folder
case MLST:
procMachineListing(ftpReq);
break;
// Machine listing, folder contents
case MLSD:
procMachineListingContents(ftpReq);
break;
// Site specific commands
case SITE:
procSite(ftpReq);
break;
// Structure command (obsolete)
case STRU:
procStructure(ftpReq);
break;
// Mode command (obsolete)
case MODE:
procMode(ftpReq);
break;
// Allocate command (obsolete)
case ALLO:
procAllocate(ftpReq);
break;
// Extended Port command
case EPRT:
procExtendedPort(ftpReq);
break;
// Extended Passive command
case EPSV:
procExtendedPassive(ftpReq);
break;
// SSL/TLS authentication
case AUTH:
procAuth(ftpReq);
break;
// Protected buffer size
case PBSZ:
procProtectedBufferSize(ftpReq);
break;
// Data channel protection level
case PROT:
procDataChannelProtection(ftpReq);
break;
// Clear command channel
case CCC:
procClearCommandChannel(ftpReq);
break;
// Unknown/unimplemented command
default:
if (ftpReq.isCommand() != FTPCommand.INVALID_CMD)
sendFTPResponse(502, "Command " + ftpReq.isCommand().name() + " not implemented");
else
sendFTPResponse(502, "Command not implemented");
break;
}
// Debug
if (Debug.EnableInfo && hasDebug(FTPSrvSession.Dbg.TIMING)) {
endTime = System.currentTimeMillis();
long duration = endTime - startTime;
if (duration > 20)
debugPrintln("Processed cmd " + ftpReq.isCommand().name() + " in " + duration + "ms");
}
// Commit/rollback a transaction that the filesystem driver may have stored in the
// session
endTransaction();
} // end while state
}
catch (SocketException ex) {
// DEBUG
if (Debug.EnableWarn && hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Socket closed by remote client");
}
catch (Exception ex) {
// Output the exception details
if (isShutdown() == false) {
debugPrintln(ex);
}
}
// Cleanup the session, make sure all resources are released
closeSession();
// Debug
if (hasDebug(FTPSrvSession.Dbg.STATE))
debugPrintln("Server session closed");
}
}