/* * This file is part of DrFTPD, Distributed FTP Daemon. * * DrFTPD is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * DrFTPD 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with DrFTPD; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.drftpd.commandmanager; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Map.Entry; import org.apache.log4j.Logger; import org.bushe.swing.event.annotation.AnnotationProcessor; import org.bushe.swing.event.annotation.EventSubscriber; import org.drftpd.event.UnloadPluginEvent; import org.drftpd.exceptions.FatalException; import org.drftpd.master.Session; import org.drftpd.util.CommonPluginUtils; import org.drftpd.util.ExtendedPropertyResourceBundle; import org.drftpd.util.PluginObjectContainer; import org.drftpd.vfs.DirectoryHandle; import org.tanesha.replacer.FormatterException; import org.tanesha.replacer.ReplacerEnvironment; import org.tanesha.replacer.SimplePrintf; /** * @author djb61 * @version $Id: StandardCommandManager.java 2418 2011-05-17 12:42:47Z cyber1331 $ */ public class StandardCommandManager implements CommandManagerInterface { private static final Logger logger = Logger .getLogger(StandardCommandManager.class); private static final String _defaultThemeDir = "conf/themes/ftp"; private static HashMap<String,CommandResponse> _genericResponses = initGenericResponses(); private ExtendedPropertyResourceBundle _defaultTheme; private ExtendedPropertyResourceBundle _theme; private ExtendedPropertyResourceBundle _fallbackTheme; /** * This is a map of commands, e.x.: * "AUTH" -> CommandInstanceContainer (Instance of the CommandInterface and the appropriately attached Method) */ private Map<String, CommandInstanceContainer> _commands; public StandardCommandManager() { // Subscribe to events AnnotationProcessor.process(this); } public synchronized void initialize(HashMap<String,Properties> requiredCmds, String themeDir) { loadThemes(themeDir); HashMap<String,CommandInstanceContainer> commands = new HashMap<String,CommandInstanceContainer>(); /* Iterate over the ArrayList of commands that the calling frontend * has stated it needs. Check to see whether we have a valid Command * extension attached for the command, is so add it to the commands * map to be used */ for (Entry<String,Properties> requiredCmd : requiredCmds.entrySet()) { String methodString = requiredCmd.getValue().getProperty("method"); String classString = requiredCmd.getValue().getProperty("class"); String pluginString = requiredCmd.getValue().getProperty("plugin"); if (methodString == null || classString == null || pluginString == null) { throw new FatalException( "Cannot load command " + requiredCmd.getKey() + ", make sure method, class, and plugin are all specified"); } try { PluginObjectContainer<CommandInterface> container = CommonPluginUtils.getSinglePluginObjectInContainer(this, "org.drftpd.commandmanager", "Command", pluginString+"."+classString, methodString, pluginString, new Class[] { CommandRequest.class }); CommandInterface cmdInstance = container.getPluginObject(); cmdInstance.initialize(methodString, pluginString, this); commands.put(requiredCmd.getKey(),new CommandInstanceContainer(container.getPluginMethod(),cmdInstance)); logger.debug("Adding CommandInstance " + requiredCmd.getKey()); } catch(Exception e) { /* Should be safe to continue, just means this command class won't be * available */ logger.info("Failed to add command handler: "+requiredCmd, e); } } _commands = commands; } private void loadThemes(String themeDir) { // Load the default theme for the frontend as well as any user overrides FileInputStream defaultIs = null; try { defaultIs = new FileInputStream(new File(themeDir+File.separator+"core.theme.default")); _defaultTheme = new ExtendedPropertyResourceBundle(defaultIs); } catch (FileNotFoundException e) { logger.error("No default theme file core.theme.default found in: "+themeDir,e); } catch (IOException e) { logger.error("Error loading core.theme.default from: "+themeDir,e); } finally { try { if (defaultIs != null) { defaultIs.close(); } } catch (IOException e) { // FileInputStream already closed } } FileInputStream userIs = null; try { userIs = new FileInputStream(new File(themeDir+File.separator+"core.theme")); _theme = new ExtendedPropertyResourceBundle(userIs); } catch (FileNotFoundException e) { // Means the user hasn't specified any overrides, since we can't have an // empty bundle, just point this to the default _theme = _defaultTheme; } catch (IOException e) { // Means the user did specify overrides but they can't be loaded, point // to the default to keep things functional but log a warning to make // the user aware of the problem _theme = _defaultTheme; logger.warn("Error loading core.theme from: "+themeDir,e); } finally { try { if (userIs != null) { userIs.close(); } } catch (IOException e) { // FileInputStream already closed } } // Set the parent bundle to the default to allow access to any non-overriden values // if there is an override file if (!_theme.equals(_defaultTheme)) { _theme.setParent(_defaultTheme); } // If this isn't the base ftp frontend then add the default ftp theme as a final // fallback theme if (!themeDir.equals(_defaultThemeDir)) { FileInputStream fallbackIs = null; try { fallbackIs = new FileInputStream(new File(_defaultThemeDir+File.separator+"core.theme.default")); _fallbackTheme = new ExtendedPropertyResourceBundle(fallbackIs); } catch (FileNotFoundException e) { logger.error("Base ftp default theme not found: "+_defaultThemeDir+File.separator+"core.theme.default",e); } catch (IOException e) { logger.error("Error loading base ftp default theme: "+_defaultThemeDir+File.separator+"core.theme.default",e); } finally { if (fallbackIs != null) { try { fallbackIs.close(); } catch (IOException e) { logger.debug("could not close fallbackIs",e); } } } _defaultTheme.setParent(_fallbackTheme); } } public CommandResponseInterface execute(CommandRequestInterface request) { CommandInstanceContainer commandContainer = _commands.get(request.getCommand()); if (commandContainer == null) { CommandResponseInterface cmdFailed = genericResponse("RESPONSE_502_COMMAND_NOT_IMPLEMENTED"); return cmdFailed; } request.setProperties(request.getSession().getCommands().get(request.getCommand())); CommandResponseInterface response = null; request = commandContainer.getCommandInterfaceInstance().doPreHooks(request); if(!request.isAllowed()) { response = request.getDeniedResponse(); if (response == null) { response = StandardCommandManager.genericResponse("RESPONSE_530_ACCESS_DENIED"); } return response; } try { try { response = (CommandResponseInterface) commandContainer.getMethod().invoke(commandContainer.getCommandInterfaceInstance(), new Object[] {request}); } catch (InvocationTargetException e) { throw e.getCause(); } } catch (ImproperUsageException e) { response = StandardCommandManager.genericResponse("RESPONSE_501_SYNTAX_ERROR"); String helpString = request.getProperties().getProperty("help.specific"); if (helpString == null) { response.addComment("Bug your siteop to add help for the \"" + request.getCommand() + "\" command"); } else { ReplacerEnvironment env = new ReplacerEnvironment(); env.add("command", request.getCommand().toUpperCase()); try { response.addComment(SimplePrintf.jprintf(helpString,env)); } catch (FormatterException e1) { response.addComment(request.getCommand().toUpperCase() + " command has an invalid help.specific definition"); } } } catch (Throwable t) { if (!(t instanceof Error)) { CommandResponseInterface cmdFailed = new CommandResponse(540, "Command execution failed"); logger.error("Command "+request.getCommand()+" failed: '" + request.getArgument() + "'", t); return cmdFailed; } throw (Error) t; } commandContainer.getCommandInterfaceInstance().doPostHooks(request, response); return response; } public static CommandResponse genericResponse(String type) { try { return (CommandResponse) _genericResponses.get(type).clone(); } catch (NullPointerException e) { logger.error("An unknown generic FTP response was used: "+type+ " this is almost certainly a bug"); return new CommandResponse(540, "No response message defined"); } } public static CommandResponse genericResponse(String type, DirectoryHandle directory, String user) { CommandResponse response = null; response = genericResponse(type); response.setCurrentDirectory(directory); response.setUser(user); return response; } private static HashMap<String,CommandResponse> initGenericResponses() { HashMap<String,CommandResponse> genericResponses = new HashMap<String,CommandResponse>(); /** 150 File status okay; about to open data connection. */ genericResponses.put("RESPONSE_150_OK", new CommandResponse(150,"File status okay; about to open data connection.")); /** 200 Command okay */ genericResponses.put("RESPONSE_200_COMMAND_OK", new CommandResponse(200,"Command okay")); /** 202 Command not implemented, superfluous at this site. */ genericResponses.put("RESPONSE_202_COMMAND_NOT_IMPLEMENTED", new CommandResponse(202, "Command not implemented, superfluous at this site.")); /** 215 NAME system type. */ genericResponses.put("RESPONSE_215_SYSTEM_TYPE", new CommandResponse(215,"UNIX system type.")); /** 221 Service closing control connection. */ genericResponses.put("RESPONSE_221_SERVICE_CLOSING", new CommandResponse(221,"Service closing control connection.")); /** 226 Closing data connection */ genericResponses.put("RESPONSE_226_CLOSING_DATA_CONNECTION", new CommandResponse(226, "Closing data connection")); /** 230 User logged in, proceed. */ genericResponses.put("RESPONSE_230_USER_LOGGED_IN", new CommandResponse(230,"User logged in, proceed.")); /** 250 Requested file action okay, completed. */ genericResponses.put("RESPONSE_250_ACTION_OKAY", new CommandResponse(250,"Requested file action okay, completed.")); /** 331 User name okay, need password. */ genericResponses.put("RESPONSE_331_USERNAME_OK_NEED_PASS", new CommandResponse(331, "User name okay, need password.")); /** 350 Requested file action pending further information. */ genericResponses.put("RESPONSE_350_PENDING_FURTHER_INFORMATION", new CommandResponse(350, "Requested file action pending further information.")); /** 425 Can't open data connection. */ genericResponses.put("RESPONSE_425_CANT_OPEN_DATA_CONNECTION", new CommandResponse(425, "Can't open data connection.\r\n")); /** 426 Connection closed; transfer aborted. */ genericResponses.put("RESPONSE_426_CONNECTION_CLOSED_TRANSFER_ABORTED", new CommandResponse(426, "Connection closed; transfer aborted.")); /** 450 Requested file action not taken. */ genericResponses.put("RESPONSE_450_REQUESTED_ACTION_NOT_TAKEN", new CommandResponse(450, "Requested file action not taken.")); /** * 450 No transfer-slave(s) available author <a * href="mailto:drftpd@mog.se">Morgan Christiansson</a> */ genericResponses.put("RESPONSE_450_SLAVE_UNAVAILABLE", new CommandResponse(450,"No transfer-slave(s) available")); /** 500 Syntax error, command unrecognized. */ genericResponses.put("RESPONSE_500_SYNTAX_ERROR", new CommandResponse(500,"Syntax error, command unrecognized.")); /** 501 Syntax error in parameters or arguments */ genericResponses.put("RESPONSE_501_SYNTAX_ERROR", new CommandResponse(501,"Syntax error in parameters or arguments")); /** 502 Command not implemented. */ genericResponses.put("RESPONSE_502_COMMAND_NOT_IMPLEMENTED", new CommandResponse(502, "Command not implemented.")); /** 503 Bad sequence of commands. */ genericResponses.put("RESPONSE_503_BAD_SEQUENCE_OF_COMMANDS", new CommandResponse(503, "Bad sequence of commands.")); /** 504 Command not implemented for that parameter. */ genericResponses.put("RESPONSE_504_COMMAND_NOT_IMPLEMENTED_FOR_PARM", new CommandResponse(504, "Command not implemented for that parameter.")); /** 530 Access denied */ genericResponses.put("RESPONSE_530_ACCESS_DENIED", new CommandResponse(530,"Access denied")); /** 530 Not logged in. */ genericResponses.put("RESPONSE_530_NOT_LOGGED_IN", new CommandResponse(530,"Not logged in.")); genericResponses.put("RESPONSE_530_SLAVE_UNAVAILABLE", new CommandResponse(530,"No transfer-slave(s) available")); /** * 550 Requested action not taken. File unavailable. File unavailable (e.g., * file not found, no access). */ genericResponses.put("RESPONSE_550_REQUESTED_ACTION_NOT_TAKEN", new CommandResponse(550, "Requested action not taken. File unavailable (e.g., file not found, no access)")); /** * 553 Requested action not taken. File name not allowed. */ genericResponses.put("RESPONSE_553_REQUESTED_ACTION_NOT_TAKEN", new CommandResponse(553, "Requested action not taken. File name not allowed")); /** * 550 Requested action not taken. File exists. */ genericResponses.put("RESPONSE_553_REQUESTED_ACTION_NOT_TAKEN_FILE_EXISTS", new CommandResponse(553, "Requested action not taken. File exists.")); return genericResponses; } public CommandRequestInterface newRequest(String argument, String command, DirectoryHandle directory, String user) { return new CommandRequest(argument, command, directory, user); } public CommandRequestInterface newRequest(String originalCommand, String argument, DirectoryHandle directory, String user, Session session, Properties config) { return new CommandRequest(originalCommand, argument, directory, user, session, config); } public Map<String,CommandInstanceContainer> getCommandHandlersMap() { return _commands; } @EventSubscriber public synchronized void onUnloadPluginEvent(UnloadPluginEvent event) { HashMap<String,CommandInstanceContainer> clonedCommands = null; String currentPlugin = CommonPluginUtils.getPluginIdForObject(this); for (String pluginExtension : event.getParentPlugins()) { int pointIndex = pluginExtension.lastIndexOf("@"); String plugin = pluginExtension.substring(0, pointIndex); String extension = pluginExtension.substring(pointIndex+1); if (plugin.equals(currentPlugin) && extension.equals("Command")) { if (clonedCommands == null) { clonedCommands = new HashMap<String,CommandInstanceContainer>(_commands); } boolean removedCmd = false; for (Iterator<Entry<String,CommandInstanceContainer>> iter = clonedCommands.entrySet().iterator(); iter.hasNext();) { Entry<String, CommandInstanceContainer> entry = iter.next(); if (CommonPluginUtils.getPluginIdForObject(entry.getValue().getCommandInterfaceInstance()).equals(event.getPlugin())) { logger.debug("Removing command "+ entry.getKey()); iter.remove(); entry.getValue().getCommandInterfaceInstance().unload(); removedCmd = true; } } if (removedCmd) { _commands = clonedCommands; } } } } public ExtendedPropertyResourceBundle getResourceBundle() { return _theme; } }