/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.server; import tigase.conf.Configurable; import tigase.disco.ServiceEntity; import tigase.disco.ServiceIdentity; import tigase.disco.XMPPService; import tigase.server.script.AddScriptCommand; import tigase.server.script.CommandIfc; import tigase.server.script.RemoveScriptCommand; import tigase.util.DNSResolver; import tigase.util.TigaseStringprepException; import tigase.vhosts.VHostItem; import tigase.vhosts.VHostListener; import tigase.vhosts.VHostManagerIfc; import tigase.xml.Element; import tigase.xmpp.Authorization; import tigase.xmpp.BareJID; import tigase.xmpp.JID; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.Arrays; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.script.Bindings; import javax.script.ScriptEngineManager; /** * Created: Oct 17, 2009 7:49:05 PM * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class BasicComponent implements Configurable, XMPPService, VHostListener { /** Field description */ public static final String SCRIPTS_DIR_PROP_DEF = "scripts/admin"; /** Field description */ public static final String SCRIPTS_DIR_PROP_KEY = "scripts-dir"; /** Field description */ public static final String COMMAND_PROP_NODE = "command"; /** Field description */ public static final String ALL_PROP_KEY = "ALL"; /** * Variable <code>log</code> is a class logger. */ private static final Logger log = Logger.getLogger(BasicComponent.class.getName()); private String DEF_HOSTNAME_PROP_VAL = DNSResolver.getDefaultHostname(); private JID compId = null; private String name = null; private BareJID defHostname = BareJID.bareJIDInstanceNS(DEF_HOSTNAME_PROP_VAL); protected Map<String, CommandIfc> scriptCommands = new ConcurrentHashMap<String, CommandIfc>(20); private boolean nonAdminCommands = false; private Map<String, EnumSet<CmdAcl>> commandsACL = new ConcurrentHashMap<String, EnumSet<CmdAcl>>(20); /** * List of the component administrators */ protected Set<BareJID> admins = new ConcurrentSkipListSet<BareJID>(); private ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); private String scriptsBaseDir = null; private String scriptsCompDir = null; private ServiceEntity serviceEntity = null; protected VHostManagerIfc vHostManager = null; /** * Method description * * * @param domain */ public void addComponentDomain(String domain) { vHostManager.addComponentDomain(domain); } /** * * @param jid * @param commandId * @return */ public boolean canCallCommand(JID jid, String commandId) { boolean result = isAdmin(jid); if (result) { return true; } EnumSet<CmdAcl> acl = commandsACL.get(ALL_PROP_KEY); if (acl != null) { result = checkCommandAcl(jid, acl); } if (!result) { acl = commandsACL.get(commandId); if (acl != null) { result = checkCommandAcl(jid, acl); } } return result; } /** * Method description * * * @param jid * @param acl * * @return */ public boolean checkCommandAcl(JID jid, EnumSet<CmdAcl> acl) { for (CmdAcl cmdAcl : acl) { switch (cmdAcl) { case ALL: return true; case ADMIN: if (isAdmin(jid)) { return true; } break; case LOCAL: if (isLocalDomain(jid.getDomain())) { return true; } break; case DOMAIN: if (jid.getDomain().equals(cmdAcl.getAclVal())) { return true; } break; case JID: case OTHER: default: if (jid.getBareJID().toString().equals(cmdAcl.getAclVal())) { return true; } } } return false; } /** * Method description * * * @return */ @Override public JID getComponentId() { return compId; } /** * Method description * * * @return */ public BareJID getDefHostName() { return defHostname; } /** * Method description * * * @param params * * @return */ @Override public Map<String, Object> getDefaults(Map<String, Object> params) { Map<String, Object> defs = new LinkedHashMap<String, Object>(50); defs.put(COMPONENT_ID_PROP_KEY, compId.toString()); DEF_HOSTNAME_PROP_VAL = DNSResolver.getDefaultHostname(); defs.put(DEF_HOSTNAME_PROP_KEY, DEF_HOSTNAME_PROP_VAL); String[] adm = null; if (params.get(GEN_ADMINS) != null) { adm = ((String) params.get(GEN_ADMINS)).split(","); } else { adm = new String[] { "admin@localhost" }; } defs.put(ADMINS_PROP_KEY, adm); String scripts_dir = (String) params.get(GEN_SCRIPT_DIR); if (scripts_dir == null) { scripts_dir = SCRIPTS_DIR_PROP_DEF; } defs.put(SCRIPTS_DIR_PROP_KEY, scripts_dir); defs.put(COMMAND_PROP_NODE + "/" + ALL_PROP_KEY, CmdAcl.ADMIN.name()); return defs; } /** * Method description * * * @return */ public String getDiscoCategoryType() { return "generic"; } public String getDiscoCategory() { return "component"; } /** * Method description * * * @return */ public String getDiscoDescription() { return "Undefined description"; } /** * Exists for backward compatibility with the old API. * * @return */ @Deprecated public List<Element> getDiscoFeatures() { return null; } /** * Method description * * * @param from * * @return */ @Override public List<Element> getDiscoFeatures(JID from) { return getDiscoFeatures(); } /** * Exists for backward compatibility with the old API. * * @param node * @param jid * * @return */ @Deprecated public Element getDiscoInfo(String node, JID jid) { return null; } /** * Method description * * * @param node * @param jid * @param from * * @return */ @Override public Element getDiscoInfo(String node, JID jid, JID from) { // This is only to support the old depreciated API. Element result = getDiscoInfo(node, jid); if (result != null) { return result; } // OLD API support end if (getName().equals(jid.getLocalpart())) { return serviceEntity.getDiscoInfo(node, isAdmin(from) || nonAdminCommands); } return null; } /** * Exists for backward compatibility with the old API. * * @deprecated * * @param node * @param jid * * @return */ @Deprecated public List<Element> getDiscoItems(String node, JID jid) { return null; } public List<Element> getScriptItems(String node, JID jid, JID from) { LinkedList<Element> result = null; boolean isAdminFrom = isAdmin(from); if (node.equals("http://jabber.org/protocol/commands") && (isAdminFrom || nonAdminCommands)) { result = new LinkedList<Element>(); for (CommandIfc comm : scriptCommands.values()) { if (!comm.isAdminOnly() || isAdminFrom) { result .add(new Element("item", new String[] { "node", "name", "jid" }, new String[] { comm.getCommandId(), comm.getDescription(), jid.toString() })); } } } return result; } /** * Method description * * * @param node * @param jid * @param from * * @return */ @Override public List<Element> getDiscoItems(String node, JID jid, JID from) { // This is only to support the old depreciated API. List<Element> result = getDiscoItems(node, jid); if (result != null) { return result; } // OLD API support end boolean isAdminFrom = isAdmin(from); if (getName().equals(jid.getLocalpart())) { if (node != null) { result = getScriptItems(node, jid, from); } else { result = serviceEntity.getDiscoItems(null, jid.toString(), isAdminFrom || nonAdminCommands); if (result != null) { for (Iterator<Element> it = result.iterator(); it.hasNext();) { Element element = it.next(); if (element.getAttribute("node") == null) { it.remove(); } } } } // Element result = serviceEntity.getDiscoItem(null, getName() + "." + // jid); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0} Found disco items: {1}", new Object[] { getName(), ((result != null) ? result.toString() : null) }); } return result; } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0} General disco items request, node: {1}", new Object[] { getName(), node }); } if (node == null) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0} Disco items request for null node", new Object[] { getName() }); } Element res = null; if (!serviceEntity.isAdminOnly() || isAdminFrom || nonAdminCommands) { res = serviceEntity.getDiscoItem(null, BareJID.toString(getName(), jid.toString())); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "{0} not admin only or isAdmin, result: {1}", new Object[] { getName(), res }); } } result = serviceEntity.getDiscoItems(null, null, isAdminFrom || nonAdminCommands); if (res != null) { if (result != null) { for (Iterator<Element> it = result.iterator(); it.hasNext();) { Element element = it.next(); if (element.getAttribute("node") != null) { it.remove(); } } result.add(0, res); } else { result = Arrays.asList(res); } } } return result; } } /** * Method description * * * @return */ @Override public String getName() { return name; } /** * Method description * * * @param domain * * @return */ public VHostItem getVHostItem(String domain) { return (vHostManager != null) ? vHostManager.getVHostItem(domain) : null; } public BareJID getDefVHostItem() { return (vHostManager != null) ? vHostManager.getDefVHostItem() : getDefHostName(); } /** * Method description * * * @return */ @Override public boolean handlesLocalDomains() { return false; } /** * Method description * * * @return */ @Override public boolean handlesNameSubdomains() { return true; } /** * Method description * * * @return */ @Override public boolean handlesNonLocalDomains() { return false; } /** * Method description * * * @param binds */ public void initBindings(Bindings binds) { binds.put(CommandIfc.VHOST_MANAGER, vHostManager); binds.put(CommandIfc.ADMINS_SET, admins); binds.put(CommandIfc.COMMANDS_ACL, commandsACL); binds.put(CommandIfc.SCRI_MANA, scriptEngineManager); binds.put(CommandIfc.ADMN_CMDS, scriptCommands); binds.put(CommandIfc.ADMN_DISC, serviceEntity); binds.put(CommandIfc.SCRIPT_BASE_DIR, scriptsBaseDir); binds.put(CommandIfc.SCRIPT_COMP_DIR, scriptsCompDir); binds.put(CommandIfc.COMPONENT_NAME, getName()); } /** * Method description * */ @Override public void initializationCompleted() { } /** * Method description * * * @param jid * * @return */ public boolean isAdmin(JID jid) { return admins.contains(jid.getBareJID()); } /** * Method description * * * @param domain * * @return */ public boolean isLocalDomain(String domain) { return (vHostManager != null) ? vHostManager.isLocalDomain(domain) : false; } /** * Method description * * * @param domain * * @return */ public boolean isLocalDomainOrComponent(String domain) { return (vHostManager != null) ? vHostManager.isLocalDomainOrComponent(domain) : false; } /** * Method description * * * @param packet * @param results */ @Override public void processPacket(Packet packet, Queue<Packet> results) { if (packet.isCommand() && getName().equals(packet.getStanzaTo().getLocalpart()) && isLocalDomain(packet.getStanzaTo().getDomain())) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Command addressed to: {0}, command: {1}", new Object[] { getName(), packet }); } processScriptCommand(packet, results); } } /** * Method description * */ @Override public void release() { } /** * Method description * * * @param domain */ public void removeComponentDomain(String domain) { vHostManager.removeComponentDomain(domain); } /** * Method description * * * @param jid * @param node * @param description */ public void removeServiceDiscoveryItem(String jid, String node, String description) { ServiceEntity item = new ServiceEntity(jid, node, description); // item.addIdentities(new ServiceIdentity("component", identity_type, // name)); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Modifying service-discovery info, removing: {0}", item); } serviceEntity.removeItems(item); } /** * Method description * * * @param name */ @Override public void setName(String name) { this.name = name; try { compId = JID.jidInstance(name, defHostname.getDomain(), null); } catch (TigaseStringprepException ex) { log.log(Level.WARNING, "Problem setting component ID: ", ex); } } /** * Method description * * * @param props */ @Override public void setProperties(Map<String, Object> props) { if (props.get(COMPONENT_ID_PROP_KEY) != null) { try { compId = JID.jidInstance((String) props.get(COMPONENT_ID_PROP_KEY)); } catch (TigaseStringprepException ex) { log.log(Level.WARNING, "Problem setting component ID: ", ex); } } if (props.get(DEF_HOSTNAME_PROP_KEY) != null) { defHostname = BareJID.bareJIDInstanceNS((String) props.get(DEF_HOSTNAME_PROP_KEY)); } String[] admins_tmp = (String[]) props.get(ADMINS_PROP_KEY); if (admins_tmp != null) { for (String admin : admins_tmp) { try { admins.add(BareJID.bareJIDInstance(admin)); } catch (TigaseStringprepException ex) { log.log(Level.CONFIG, "Incorrect admin JID: ", ex); } } } for (Map.Entry<String, Object> entry : props.entrySet()) { if (entry.getKey().startsWith(COMMAND_PROP_NODE)) { String cmdId = entry.getKey().substring(COMMAND_PROP_NODE.length() + 1); String[] cmdAcl = entry.getValue().toString().split(","); EnumSet<CmdAcl> acl = EnumSet.noneOf(CmdAcl.class); for (String cmda : cmdAcl) { CmdAcl acl_tmp = CmdAcl.valueof(cmda); acl.add(acl_tmp); if (acl_tmp != CmdAcl.ADMIN) { nonAdminCommands = true; } } commandsACL.put(cmdId, acl); } } serviceEntity = new ServiceEntity(name, null, getDiscoDescription(), true); serviceEntity.addIdentities(new ServiceIdentity(getDiscoCategory(), getDiscoCategoryType(), getDiscoDescription())); serviceEntity.addFeatures("http://jabber.org/protocol/commands"); CommandIfc command = new AddScriptCommand(); command.init(CommandIfc.ADD_SCRIPT_CMD, "New command script"); scriptCommands.put(command.getCommandId(), command); command = new RemoveScriptCommand(); command.init(CommandIfc.DEL_SCRIPT_CMD, "Remove command script"); scriptCommands.put(command.getCommandId(), command); if (props.get(SCRIPTS_DIR_PROP_KEY) != null) { scriptsBaseDir = (String) props.get(SCRIPTS_DIR_PROP_KEY); scriptsCompDir = scriptsBaseDir + "/" + getName(); loadScripts(); } } /** * Method description * * * @param manager */ @Override public void setVHostManager(VHostManagerIfc manager) { this.vHostManager = manager; } /** * Method description * * * @param jid * @param node * @param description * @param admin */ public void updateServiceDiscoveryItem(String jid, String node, String description, boolean admin) { updateServiceDiscoveryItem(jid, node, description, admin, (String[]) null); } /** * Method description * * * @param jid * @param node * @param description * @param admin * @param features */ public void updateServiceDiscoveryItem(String jid, String node, String description, boolean admin, String... features) { updateServiceDiscoveryItem(jid, node, description, null, null, admin, features); } /** * Method description * * * @param jid * @param node * @param description * @param category * @param type * @param admin * @param features */ public void updateServiceDiscoveryItem(String jid, String node, String description, String category, String type, boolean admin, String... features) { if (serviceEntity.getJID().equals(jid) && (serviceEntity.getNode() == node)) { serviceEntity.setAdminOnly(admin); serviceEntity.setDescription(description); if ((category != null) || (type != null)) { serviceEntity.addIdentities(new ServiceIdentity(category, type, description)); } if (features != null) { serviceEntity.setFeatures("http://jabber.org/protocol/commands"); serviceEntity.addFeatures(features); } if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Modifying service-discovery info: {0}", serviceEntity); } } else { ServiceEntity item = new ServiceEntity(jid, node, description, admin); if ((category != null) || (type != null)) { item.addIdentities(new ServiceIdentity(category, type, description)); } if (features != null) { item.addFeatures(features); } if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Adding new item: {0}", item); } serviceEntity.addItems(item); } } protected boolean processScriptCommand(Packet pc, Queue<Packet> results) { // TODO: test if this is right // It is not, the packet should actually have packetFrom set at all times // to ensure the error can be sent back to the original sender. // if ((pc.getStanzaFrom() == null) || (pc.getPacketFrom() != null)) { // // // The packet has not gone through session manager yet // return false; // } // This test is more correct as it says whether the packet went through // session manager checking. // TODO: test if commands still work for users from different XMPP servers // with the right permission set. if (pc.getPermissions() == Permissions.NONE) { return false; } Iq iqc = (Iq) pc; Command.Action action = Command.getAction(iqc); if (action == Command.Action.cancel) { Packet result = iqc.commandResult(Command.DataType.result); Command.addTextField(result, "Note", "Command canceled."); results.offer(result); return true; } String strCommand = iqc.getStrCommand(); CommandIfc com = scriptCommands.get(strCommand); if ((strCommand != null) && (com != null)) { boolean admin = false; try { admin = canCallCommand(iqc.getStanzaFrom(), strCommand); if (admin) { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Processing admin command: {0}", pc); } Bindings binds = com.getBindings(); if (binds == null) { binds = scriptEngineManager.getBindings(); } // Bindings binds = scriptEngineManager.getBindings(); initBindings(binds); com.runCommand(iqc, binds, results); } else { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Command rejected non-admin detected: {0}", pc.getStanzaFrom()); } results.offer(Authorization.FORBIDDEN.getResponseMessage(pc, "Only Administrator can call the command.", true)); } } catch (Exception e) { log.log(Level.WARNING, "Unknown admin command processing exception: " + pc, e); } return true; } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "No such command: {0}, ignoring packet: {1}", new Object[] { strCommand, pc }); } } return false; } private void loadScripts() { log.log(Level.CONFIG, "Loading admin scripts for component: {0}.", new Object[] { getName() }); File file = null; AddScriptCommand addCommand = new AddScriptCommand(); Bindings binds = scriptEngineManager.getBindings(); initBindings(binds); String[] dirs = new String[] { scriptsBaseDir, scriptsCompDir }; for (String scriptsPath : dirs) { log.log(Level.CONFIG, "{0}: Loading scripts from directory: {1}", new Object[] { getName(), scriptsPath }); try { File adminDir = new File(scriptsPath); if ((adminDir != null) && adminDir.exists()) { for (File f : adminDir.listFiles()) { // Just regular files here.... if (f.isFile() && !f.toString().endsWith("~")) { String cmdId = null; String cmdDescr = null; String comp = null; file = f; StringBuilder sb = new StringBuilder(); BufferedReader buffr = new BufferedReader(new FileReader(file)); String line = null; while ((line = buffr.readLine()) != null) { sb.append(line).append("\n"); int idx = line.indexOf(CommandIfc.SCRIPT_DESCRIPTION); if (idx >= 0) { cmdDescr = line.substring(idx + CommandIfc.SCRIPT_DESCRIPTION.length()).trim(); } idx = line.indexOf(CommandIfc.SCRIPT_ID); if (idx >= 0) { cmdId = line.substring(idx + CommandIfc.SCRIPT_ID.length()).trim(); } idx = line.indexOf(CommandIfc.SCRIPT_COMPONENT); if (idx >= 0) { comp = line.substring(idx + CommandIfc.SCRIPT_COMPONENT.length()).trim(); } } buffr.close(); if ((cmdId == null) || (cmdDescr == null) || (comp == null)) { log.log(Level.WARNING, "Admin script found but it has no command ID or command" + "description: " + "{0}", file); continue; } // What components should load the script.... String[] comp_names = comp.split(","); boolean found = false; for (String cmp : comp_names) { found = getName().equals(cmp); if (found) { break; } } if (!found) { log.log(Level.CONFIG, "{0}: skipping admin script for component: {1}", new Object[] { getName(), comp }); continue; } int idx = file.toString().lastIndexOf('.'); String ext = file.toString().substring(idx + 1); addCommand.addAdminScript(cmdId, cmdDescr, sb.toString(), null, ext, binds); log.log( Level.CONFIG, "{0}: Loaded admin command from file: {1}, id: {2}, ext: {3}, descr: {4}", new Object[] { getName(), file, cmdId, ext, cmdDescr }); } } } else { log.log(Level.CONFIG, "Admin scripts directory is missing: {0}, creating...", adminDir); try { adminDir.mkdirs(); } catch (Exception e) { log.log(Level.WARNING, "Can't create scripts directory , read-only filesystem: " + file, e); } } } catch (Exception e) { log.log(Level.WARNING, "Can't load the admin script file: " + file, e); } } } }