/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * 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.jivesoftware.openfire.commands; import org.dom4j.Element; import org.dom4j.QName; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.StringUtils; import org.xmpp.forms.DataForm; import org.xmpp.forms.FormField; import org.xmpp.packet.IQ; import org.xmpp.packet.PacketError; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * An AdHocCommandManager is responsible for keeping the list of available commands offered * by a service and for processing commands requests. Typically, instances of this class * are private to the service offering ad-hoc commands. * * @author Gaston Dombiak */ public class AdHocCommandManager { private static final String NAMESPACE = "http://jabber.org/protocol/commands"; /** * Map that holds the offered commands by this service. Note: Key=commandCode, Value=command. * commandCode matches the node attribute sent by command requesters. */ private Map<String, AdHocCommand> commands = new ConcurrentHashMap<>(); /** * Map that holds the number of command sessions of each requester. * Note: Key=requester full's JID, Value=number of sessions */ private Map<String, AtomicInteger> sessionsCounter = new ConcurrentHashMap<>(); /** * Map that holds the command sessions. Used mainly to quickly locate a SessionData. * Note: Key=sessionID, Value=SessionData */ private Map<String, SessionData> sessions = new ConcurrentHashMap<>(); /** * Adds a new command to the list of supported ad-hoc commands by this server. The new * command will appear in the discoverable items list and will be executed for those users * with enough permission. * * @param command the new ad-hoc command to add. */ public void addCommand(AdHocCommand command) { commands.put(command.getCode(), command); } /** * Removes the command from the list of ad-hoc commands supported by this server. The command * will no longer appear in the discoverable items list. * * @param command the ad-hoc command to remove. * @return true if the requested command was removed from the list of available commands. */ public boolean removeCommand(AdHocCommand command) { return commands.remove(command.getCode()) != null; } /** * Returns a list with the available commands in this command manager. * * @return a list with the available commands in this command manager. */ public Collection<AdHocCommand> getCommands() { return commands.values(); } /** * Returns the command whose code matches the specified code or <tt>null</tt> if none * was found. * * @param code the code of the command to find. * @return the command whose code matches the specified code or null if none * was found. */ public AdHocCommand getCommand(String code) { return commands.get(code); } public IQ process(IQ packet) { IQ reply = IQ.createResultIQ(packet); Element iqCommand = packet.getChildElement(); // Only packets of type SET can be processed if (!IQ.Type.set.equals(packet.getType())) { // Answer a bad_request error reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.bad_request); return reply; } String sessionid = iqCommand.attributeValue("sessionid"); String commandCode = iqCommand.attributeValue("node"); String from = packet.getFrom().toString(); AdHocCommand command = commands.get(commandCode); if (sessionid == null) { // A new execution request has been received. Check that the command exists if (command == null) { // Requested command does not exist so return item_not_found error. reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.item_not_found); } else { // Check that the requester has enough permission. Answer forbidden error if // requester permissions are not enough to execute the requested command if (!command.hasPermission(packet.getFrom())) { reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.forbidden); return reply; } // Create new session ID sessionid = StringUtils.randomString(15); Element childElement = reply.setChildElement("command", NAMESPACE); if (command.getMaxStages(null) == 0) { // The command does not require any user interaction (returns results only) // Execute the command and return the execution result which may be a // data form (i.e. report data) or a note element command.execute(null, childElement); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.completed.name()); } else { // The command requires user interactions (ie. has stages) // Check that the user has not excedded the limit of allowed simultaneous // command sessions. AtomicInteger counter = sessionsCounter.get(from); if (counter == null) { synchronized (from.intern()) { counter = sessionsCounter.get(from); if (counter == null) { counter = new AtomicInteger(0); sessionsCounter.put(from, counter); } } } int limit = JiveGlobals.getIntProperty("xmpp.command.limit", 100); if (counter.incrementAndGet() > limit) { counter.decrementAndGet(); // Answer a not_allowed error since the user has exceeded limit. This // checking prevents bad users from consuming all the system memory by not // allowing them to create infinite simultaneous command sessions. reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.not_allowed); return reply; } // Originate a new command session. SessionData session = new SessionData(sessionid, packet.getFrom()); sessions.put(sessionid, session); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.executing.name()); // Add to the child element the data form the user must complete and // the allowed actions command.addNextStageInformation(session, childElement); } } } else { // An execution session already exists and the user has requested to perform a // certain action. String action = iqCommand.attributeValue("action"); SessionData session = sessions.get(sessionid); // Check that a Session exists for the specified sessionID if (session == null) { // Answer a bad_request error (bad-sessionid) reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.bad_request); return reply; } // Check if the Session data has expired (default is 10 minutes) int timeout = JiveGlobals.getIntProperty("xmpp.command.timeout", 10 * 60 * 1000); if (System.currentTimeMillis() - session.getCreationStamp() > timeout) { // TODO Check all sessions that might have timed out (use another thread?) // Remove the old session removeSessionData(sessionid, from); // Answer a not_allowed error (session-expired) reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.not_allowed); return reply; } synchronized (sessionid.intern()) { // Check if the user is requesting to cancel the command if (AdHocCommand.Action.cancel.name().equals(action)) { // User requested to cancel command execution so remove the session data removeSessionData(sessionid, from); // Generate a canceled confirmation response Element childElement = reply.setChildElement("command", NAMESPACE); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.canceled.name()); } // If the user didn't specify an action then follow the default execute action if (action == null || AdHocCommand.Action.execute.name().equals(action)) { action = session.getExecuteAction().name(); } // Check that the specified action was previously offered if (!session.isValidAction(action)) { // Answer a bad_request error (bad-action) reply.setChildElement(iqCommand.createCopy()); reply.setError(PacketError.Condition.bad_request); return reply; } else if (AdHocCommand.Action.prev.name().equals(action)) { // Move to the previous stage and add to the child element the data form // the user must complete and the allowed actions of the previous stage Element childElement = reply.setChildElement("command", NAMESPACE); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.executing.name()); command.addPreviousStageInformation(session, childElement); } else if (AdHocCommand.Action.next.name().equals(action)) { // Store the completed form in the session data saveCompletedForm(iqCommand, session); // Move to the next stage and add to the child element the new data form // the user must complete and the new allowed actions Element childElement = reply.setChildElement("command", NAMESPACE); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.executing.name()); command.addNextStageInformation(session, childElement); } else if (AdHocCommand.Action.complete.name().equals(action)) { // Store the completed form in the session data saveCompletedForm(iqCommand, session); // Execute the command and return the execution result which may be a // data form (i.e. report data) or a note element Element childElement = reply.setChildElement("command", NAMESPACE); command.execute(session, childElement); childElement.addAttribute("sessionid", sessionid); childElement.addAttribute("node", commandCode); childElement.addAttribute("status", AdHocCommand.Status.completed.name()); // Command has been executed so remove the session data removeSessionData(sessionid, from); } } } return reply; } /** * Stores in the SessionData the fields and their values as specified in the completed * data form by the user. * * @param iqCommand the command element containing the data form element. * @param session the SessionData for this command execution. */ private void saveCompletedForm(Element iqCommand, SessionData session) { Element formElement = iqCommand.element(QName.get("x", "jabber:x:data")); if (formElement != null) { // Generate a Map with the variable names and variables values Map<String, List<String>> data = new HashMap<>(); DataForm dataForm = new DataForm(formElement); for (FormField field : dataForm.getFields()) { data.put(field.getVariable(), field.getValues()); } // Store the variables and their values in the session data session.addStageForm(data); } } /** * Releases the data kept for the command execution whose id is sessionid. The number of * commands executions currently being executed by the user (full JID) will be decreased. * * @param sessionid id of the session that identifies this command execution. * @param from the full JID of the command requester. */ private void removeSessionData(String sessionid, String from) { sessions.remove(sessionid); if (sessionsCounter.get(from).decrementAndGet() <= 0) { // Remove the AtomicInteger when no commands are being executed sessionsCounter.remove(from); } } public void stop() { // Cancel executions of running commands sessions.clear(); sessionsCounter.clear(); } }