/* * Copyright 2008 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mockftpserver.fake.command; import org.mockftpserver.core.CommandSyntaxException; import org.mockftpserver.core.IllegalStateException; import org.mockftpserver.core.NotLoggedInException; import org.mockftpserver.core.command.AbstractCommandHandler; import org.mockftpserver.core.command.Command; import org.mockftpserver.core.command.ReplyCodes; import org.mockftpserver.core.session.Session; import org.mockftpserver.core.session.SessionKeys; import org.mockftpserver.core.util.Assert; import org.mockftpserver.fake.ServerConfiguration; import org.mockftpserver.fake.ServerConfigurationAware; import org.mockftpserver.fake.UserAccount; import org.mockftpserver.fake.filesystem.FileSystem; import org.mockftpserver.fake.filesystem.FileSystemEntry; import org.mockftpserver.fake.filesystem.FileSystemException; import org.mockftpserver.fake.filesystem.InvalidFilenameException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.MissingResourceException; /** * Abstract superclass for CommandHandler classes for the "Fake" server. * * @author Chris Mair * @version $Revision$ - $Date$ */ public abstract class AbstractFakeCommandHandler extends AbstractCommandHandler implements ServerConfigurationAware { protected static final String INTERNAL_ERROR_KEY = "internalError"; private ServerConfiguration serverConfiguration; /** * Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)} * This defaults to ReplyCodes.EXISTING_FILE_ERROR (550). */ protected int replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR; public ServerConfiguration getServerConfiguration() { return serverConfiguration; } public void setServerConfiguration(ServerConfiguration serverConfiguration) { this.serverConfiguration = serverConfiguration; } /** * Use template method to centralize and ensure common validation */ public void handleCommand(Command command, Session session) { Assert.notNull(serverConfiguration, "serverConfiguration"); Assert.notNull(command, "command"); Assert.notNull(session, "session"); try { handle(command, session); } catch (CommandSyntaxException e) { handleException(command, session, e, ReplyCodes.COMMAND_SYNTAX_ERROR); } catch (IllegalStateException e) { handleException(command, session, e, ReplyCodes.ILLEGAL_STATE); } catch (NotLoggedInException e) { handleException(command, session, e, ReplyCodes.NOT_LOGGED_IN); } catch (InvalidFilenameException e) { handleFileSystemException(command, session, e, ReplyCodes.FILENAME_NOT_VALID, e.getPath()); } catch (FileSystemException e) { handleFileSystemException(command, session, e, replyCodeForFileSystemException, e.getPath()); } } /** * Convenience method to return the FileSystem stored in the ServerConfiguration * * @return the FileSystem */ protected FileSystem getFileSystem() { return serverConfiguration.getFileSystem(); } /** * Handle the specified command for the session. All checked exceptions are expected to be wrapped or handled * by the caller. * * @param command - the Command to be handled * @param session - the session on which the Command was submitted */ protected abstract void handle(Command command, Session session); // ------------------------------------------------------------------------- // Utility methods for subclasses // ------------------------------------------------------------------------- /** * Send a reply for this command on the control connection. *

* The reply code is designated by the replyCode property, and the reply text * is retrieved from the replyText ResourceBundle, using the specified messageKey. * * @param session - the Session * @param replyCode - the reply code * @param messageKey - the resource bundle key for the reply text * @throws AssertionError - if session is null * @see MessageFormat */ protected void sendReply(Session session, int replyCode, String messageKey) { sendReply(session, replyCode, messageKey, Collections.EMPTY_LIST); } /** * Send a reply for this command on the control connection. *

* The reply code is designated by the replyCode property, and the reply text * is retrieved from the replyText ResourceBundle, using the specified messageKey. * * @param session - the Session * @param replyCode - the reply code * @param messageKey - the resource bundle key for the reply text * @param args - the optional message arguments; defaults to [] * @throws AssertionError - if session is null * @see MessageFormat */ protected void sendReply(Session session, int replyCode, String messageKey, List args) { Assert.notNull(session, "session"); assertValidReplyCode(replyCode); String text = getTextForKey(messageKey); String replyText = (args != null && !args.isEmpty()) ? MessageFormat.format(text, args.toArray()) : text; String replyTextToLog = (replyText == null) ? "" : " " + replyText; String argsToLog = (args != null && !args.isEmpty()) ? (" args=" + args) : ""; LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog); session.sendReply(replyCode, replyText); } /** * Send a reply for this command on the control connection. *

* The reply code is designated by the replyCode property, and the reply text * is retrieved from the replyText ResourceBundle, using the reply code as the key. * * @param session - the Session * @param replyCode - the reply code * @throws AssertionError - if session is null * @see MessageFormat */ protected void sendReply(Session session, int replyCode) { sendReply(session, replyCode, Collections.EMPTY_LIST); } /** * Send a reply for this command on the control connection. *

* The reply code is designated by the replyCode property, and the reply text * is retrieved from the replyText ResourceBundle, using the reply code as the key. * * @param session - the Session * @param replyCode - the reply code * @param args - the optional message arguments; defaults to [] * @throws AssertionError - if session is null * @see MessageFormat */ protected void sendReply(Session session, int replyCode, List args) { sendReply(session, replyCode, Integer.toString(replyCode), args); } /** * Handle the exception caught during handleCommand() * * @param command - the Command * @param session - the Session * @param exception - the caught exception * @param replyCode - the reply code that should be sent back */ private void handleException(Command command, Session session, Throwable exception, int replyCode) { LOG.warn("Error handling command: " + command + "; " + exception, exception); sendReply(session, replyCode); } /** * Handle the exception caught during handleCommand() * * @param command - the Command * @param session - the Session * @param exception - the caught exception * @param replyCode - the reply code that should be sent back * @param arg - the arg for the reply (message) */ private void handleFileSystemException(Command command, Session session, FileSystemException exception, int replyCode, Object arg) { LOG.warn("Error handling command: " + command + "; " + exception, exception); sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg)); } /** * Return the value of the named attribute within the session. * * @param session - the Session * @param name - the name of the session attribute to retrieve * @return the value of the named session attribute * @throws IllegalStateException - if the Session does not contain the named attribute */ protected Object getRequiredSessionAttribute(Session session, String name) { Object value = session.getAttribute(name); if (value == null) { throw new IllegalStateException("Session missing required attribute [" + name + "]"); } return value; } /** * Verify that the current user (if any) has already logged in successfully. * * @param session - the Session */ protected void verifyLoggedIn(Session session) { if (getUserAccount(session) == null) { throw new NotLoggedInException("User has not logged in"); } } /** * @param session - the Session * @return the UserAccount stored in the specified session; may be null */ protected UserAccount getUserAccount(Session session) { return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT); } /** * Verify that the specified condition related to the file system is true, * otherwise throw a FileSystemException. * * @param condition - the condition that must be true * @param path - the path involved in the operation; this will be included in the * error message if the condition is not true. * @param messageKey - the message key for the exception message * @throws FileSystemException - if the condition is not true */ protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) { if (!condition) { throw new FileSystemException(path, messageKey); } } /** * Verify that the current user has execute permission to the specified path * * @param session - the Session * @param path - the file system path * @throws FileSystemException - if the condition is not true */ protected void verifyExecutePermission(Session session, String path) { UserAccount userAccount = getUserAccount(session); FileSystemEntry entry = getFileSystem().getEntry(path); verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute"); } /** * Verify that the current user has write permission to the specified path * * @param session - the Session * @param path - the file system path * @throws FileSystemException - if the condition is not true */ protected void verifyWritePermission(Session session, String path) { UserAccount userAccount = getUserAccount(session); FileSystemEntry entry = getFileSystem().getEntry(path); verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite"); } /** * Verify that the current user has read permission to the specified path * * @param session - the Session * @param path - the file system path * @throws FileSystemException - if the condition is not true */ protected void verifyReadPermission(Session session, String path) { UserAccount userAccount = getUserAccount(session); FileSystemEntry entry = getFileSystem().getEntry(path); verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead"); } /** * Return the full, absolute path for the specified abstract pathname. * If path is null, return the current directory (stored in the session). If * path represents an absolute path, then return path as is. Otherwise, path * is relative, so assemble the full path from the current directory * and the specified relative path. * * @param session - the Session * @param path - the abstract pathname; may be null * @return the resulting full, absolute path */ protected String getRealPath(Session session, String path) { String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY); if (path == null) { return currentDirectory; } if (getFileSystem().isAbsolute(path)) { return path; } return getFileSystem().path(currentDirectory, path); } /** * Return the end-of-line character(s) used when building multi-line responses * * @return "\r\n" */ protected String endOfLine() { return "\r\n"; } private String getTextForKey(String key) { String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY; try { return getReplyTextBundle().getString(msgKey); } catch (MissingResourceException e) { // No reply text is mapped for the specified key LOG.warn("No reply text defined for key [" + msgKey + "]"); return null; } } // ------------------------------------------------------------------------- // Login Support (used by USER and PASS commands) // ------------------------------------------------------------------------- /** * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate * error message, and return false. A UserAccount is considered invalid if the homeDirectory property * is not set or is set to a non-existent directory. * * @param username - the username * @param session - the session; used to send back an error reply if necessary * @return true only if the UserAccount for the named user is valid */ protected boolean validateUserAccount(String username, Session session) { UserAccount userAccount = serverConfiguration.getUserAccount(username); if (userAccount == null || !userAccount.isValid()) { LOG.error("UserAccount missing or not valid for username [" + username + "]: " + userAccount); sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username)); return false; } String home = userAccount.getHomeDirectory(); if (!getFileSystem().isDirectory(home)) { LOG.error("Home directory configured for username [" + username + "] is not valid: " + home); sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home)); return false; } return true; } /** * Log in the specified user for the current session. Send back a reply of 230 with a message indicated * by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session. * * @param userAccount - the userAccount for the user to be logged in * @param session - the session * @param replyCode - the reply code to send * @param replyMessageKey - the message key for the reply text */ protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) { sendReply(session, replyCode, replyMessageKey); session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount); session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory()); } /** * Convenience method to return a List with the specified single item * * @param item - the single item in the returned List * @return a new List with that single item */ protected List list(Object item) { return Collections.singletonList(item); } /** * Convenience method to return a List with the specified two items * * @param item1 - the first item in the returned List * @param item2 - the second item in the returned List * @return a new List with the specified items */ protected List list(Object item1, Object item2) { List list = new ArrayList(2); list.add(item1); list.add(item2); return list; } /** * Return true if the specified string is null or empty * * @param string - the String to check; may be null * @return true only if the specified String is null or empyt */ protected boolean notNullOrEmpty(String string) { return string != null && string.length() > 0; } /** * Return the string unless it is null or empty, in which case return the defaultString. * * @param string - the String to check; may be null * @param defaultString - the value to return if string is null or empty * @return string if not null and not empty; otherwise return defaultString */ protected String defaultIfNullOrEmpty(String string, String defaultString) { return (notNullOrEmpty(string) ? string : defaultString); } }