/* * Copyright (C) 1999-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.plugin.spark; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicInteger; import org.dom4j.Element; import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.event.SessionEventDispatcher; import org.jivesoftware.openfire.event.SessionEventListener; import org.jivesoftware.openfire.plugin.spark.manager.SparkVersionManager; import org.jivesoftware.openfire.session.ClientSession; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.openfire.stats.StatisticsManager; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.component.Component; import org.xmpp.component.ComponentException; import org.xmpp.component.ComponentManager; import org.xmpp.component.ComponentManagerFactory; import org.xmpp.packet.IQ; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import org.xmpp.packet.PacketError; import org.xmpp.packet.StreamError; /** * Handles querying and notifications of enabled client features within the server, as well * as track related statistics, such as invalid client connections and number of Spark connections. * * @author Derek DeMoro */ public class SparkManager implements Component { private static final Logger Log = LoggerFactory.getLogger(SparkManager.class); private static final String INVALID_DISCONNECTS_KEY = "disconnects"; private static final String SPARK_CLIENTS_KEY = "spark"; private ComponentManager componentManager; private SessionManager sessionManager; private SparkSessionListener sessionEventListener; /** * Tracks number of disconnected clients. */ private AtomicInteger disconnects; private StatisticsManager statisticsManager; private TaskEngine taskEngine; /** * Defined Service Name for Component. */ private String serviceName = "manager"; /** * Creates a new instance of the SparkManager to allow for listening and responding to * newly created client sessions. * * @param taskEngine the taskEngine. */ public SparkManager(TaskEngine taskEngine) { this.taskEngine = taskEngine; sessionManager = SessionManager.getInstance(); sessionEventListener = new SparkSessionListener(); statisticsManager = StatisticsManager.getInstance(); componentManager = ComponentManagerFactory.getComponentManager(); // Register the SparkManager Component with the component manager // using the defined service name. This component is cluster-safe. try { componentManager.addComponent(serviceName, this); } catch (Exception e) { Log.error(e.getMessage(), e); } // Add VersionManager. This component is cluster-safe. try { componentManager.addComponent(SparkVersionManager.SERVICE_NAME, new SparkVersionManager()); } catch (Exception e) { Log.error(e.getMessage(), e); } // Add SessionListener SessionEventDispatcher.addListener(sessionEventListener); disconnects = new AtomicInteger(0); } public String getName() { return "Features Component"; } public String getDescription() { return "Allows for discovery of certain features."; } /** * Client features are detected using Service Discovery, allowing * for ease of use within the client. When a client "discovers" the * manager, they can query for related features within that discovered item. * * @param packet the packet */ public void processPacket(Packet packet) { if (packet instanceof IQ) { IQ iqPacket = (IQ)packet; Element childElement = (iqPacket).getChildElement(); String namespace = null; if (childElement != null) { namespace = childElement.getNamespaceURI(); } if (IQ.Type.get == iqPacket.getType()) { // Handle any disco info requests. if ("http://jabber.org/protocol/disco#info".equals(namespace)) { handleDiscoInfo(iqPacket); } // Handle any disco item requests. else if ("http://jabber.org/protocol/disco#items".equals(namespace)) { handleDiscoItems(iqPacket); } else if ("jabber:iq:version".equals(namespace)) { IQ reply = IQ.createResultIQ(iqPacket); Element version = reply.setChildElement("query", "jabber:iq:version"); version.addElement("name").setText("Client Control Manager"); version.addElement("version").setText("3.5"); sendPacket(reply); } else { // Return error since this is an unknown service request IQ reply = IQ.createResultIQ(iqPacket); reply.setError(PacketError.Condition.service_unavailable); sendPacket(reply); } } else if (IQ.Type.error == iqPacket.getType() || IQ.Type.result == iqPacket.getType()) { if ("jabber:iq:version".equals(namespace)) { handleClientVersion(iqPacket); } } else { // Return error since this is an unknown service request IQ reply = IQ.createResultIQ(iqPacket); reply.setError(PacketError.Condition.service_unavailable); sendPacket(reply); } } } /** * Handles the IQ version reply. If only a given list of clients are allowed to connect * then the reply will be analyzed. If the client is not present in the list, no name * was responsed or an IQ error was returned (e.g. IQ version not supported) then * the client session will be terminated. * * @param iq the IQ version reply sent by the client. */ private void handleClientVersion(IQ iq) { final String clientsAllowed = JiveGlobals.getProperty("clients.allowed", "all"); final boolean disconnectIfNoMatch = !"all".equals(clientsAllowed); if ("all".equals(clientsAllowed) || !disconnectIfNoMatch) { // There is nothing to do here. Just return. return; } // Get the client session of the user that sent the IQ version response ClientSession session = sessionManager.getSession(iq.getFrom()); if (session == null) { // Do nothing if the session no longer exists return; } if (IQ.Type.result == iq.getType()) { // Get list of allowed clients to connect final List<String> clients = new ArrayList<String>(); StringTokenizer clientTokens = new StringTokenizer(clientsAllowed, ","); while (clientTokens.hasMoreTokens()) { clients.add(clientTokens.nextToken().toLowerCase()); } final String otherClientsAllowed = JiveGlobals.getProperty("other.clients.allowed", ""); clientTokens = new StringTokenizer(otherClientsAllowed, ","); while (clientTokens.hasMoreTokens()) { clients.add(clientTokens.nextToken().toLowerCase().trim()); } Element child = iq.getChildElement(); String clientName = child.elementTextTrim("name"); boolean disconnect = true; if (clientName != null) { // Check if the client should be disconnected for(String c : clients){ if(clientName.toLowerCase().contains(c)){ disconnect = false; break; } } } else { // Always disconnect clients that didn't provide their name disconnect = true; } if (disconnect) { closeSession(session, clientName != null ? clientName : "Unknown"); } } else { // If the session is invalid. Close the connection. closeSession(session, "Unknown"); } } public void initialize(JID jid, ComponentManager componentManager) throws ComponentException { // Do nothing. } public void start() { // Do nothing. } /** * Unload the Component. */ public void stop() { // Unregister components try { componentManager.removeComponent(SparkVersionManager.SERVICE_NAME); // Finally remove this service (this will set null to componentManager) componentManager.removeComponent(serviceName); } catch (ComponentException e) { Log.error(e.getMessage(), e); } taskEngine = null; } /** * Remove any resources SparkManager was using. This will allow * for a clean reload. */ public void shutdown() { // Cleanup SessionEventDispatcher.removeListener(sessionEventListener); if (statisticsManager != null) { statisticsManager.removeStatistic(SPARK_CLIENTS_KEY); statisticsManager.removeStatistic(INVALID_DISCONNECTS_KEY); } componentManager = null; sessionManager = null; sessionEventListener = null; statisticsManager = null; } /** * Sends a reply for a ServiceDiscovery. * * @param packet the packet. */ private void handleDiscoItems(IQ packet) { IQ replyPacket = IQ.createResultIQ(packet); replyPacket.setChildElement("query", "http://jabber.org/protocol/disco#items"); sendPacket(replyPacket); } /** * Send a reply back to the client to inform the client that this server has * a Spark Manager. * * @param packet the IQ packet. */ private void handleDiscoInfo(IQ packet) { IQ replyPacket = IQ.createResultIQ(packet); Element responseElement = replyPacket.setChildElement("query", "http://jabber.org/protocol/disco#info"); Element identity = responseElement.addElement("identity"); identity.addAttribute("category", "manager"); identity.addAttribute("type", "text"); identity.addAttribute("name", "Client Control Manager"); // Add features set buildFeatureSet(responseElement); // Send reply sendPacket(replyPacket); } private void sendPacket(Packet packet) { try { componentManager.sendPacket(this, packet); } catch (ComponentException e) { Log.error(e.getMessage(), e); } } /** * Builds an element list of all features enabled. * * @param responseElement the feature response element. */ private void buildFeatureSet(Element responseElement) { // Check for ACCOUNT REGISTRATION feature boolean accountsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("accounts.enabled", "true")); if (accountsEnabled) { responseElement.addElement("feature").addAttribute("var", "accounts-reg"); } // Check for ADD CONTACTS feature boolean addcontactsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("addcontacts.enabled", "true")); if (addcontactsEnabled) { responseElement.addElement("feature").addAttribute("var", "add-contacts"); } // Check for ADD GROUPS feature boolean addgroupsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("addgroups.enabled", "true")); if (addgroupsEnabled) { responseElement.addElement("feature").addAttribute("var", "add-groups"); } // Check for ADVANCED CONFIGURATION feature boolean advancedEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("advanced.enabled", "true")); if (advancedEnabled) { responseElement.addElement("feature").addAttribute("var", "advanced-config"); } // Check for AVATARS feature boolean avatarsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("avatars.enabled", "true")); if (avatarsEnabled) { responseElement.addElement("feature").addAttribute("var", "avatar-tab"); } // Check for BROADCASTING feature boolean broadcastEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("broadcast.enabled", "true")); if (broadcastEnabled) { responseElement.addElement("feature").addAttribute("var", "broadcast"); } // Check for CONTACT & GROUP REMOVALS feature boolean removalsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("removals.enabled", "true")); if (removalsEnabled) { responseElement.addElement("feature").addAttribute("var", "removals"); } // Check for CONTACT & GROUP RENAMES feature boolean renamesEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("renames.enabled", "true")); if (renamesEnabled) { responseElement.addElement("feature").addAttribute("var", "renames"); } // Check for FILE TRANSFER feature boolean fileTransferEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("transfer.enabled", "true")); if (fileTransferEnabled) { responseElement.addElement("feature").addAttribute("var", "file-transfer"); } // Check for HELP FORUMS feature boolean helpforumsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("helpforums.enabled", "true")); if (helpforumsEnabled) { responseElement.addElement("feature").addAttribute("var", "help-forums"); } // Check for HELP USER GUIDE feature boolean helpuserguideEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("helpuserguide.enabled", "true")); if (helpuserguideEnabled) { responseElement.addElement("feature").addAttribute("var", "help-userguide"); } // Check for HISTORY SETTINGS feature boolean historysettingsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("historysettings.enabled", "true")); if (historysettingsEnabled) { responseElement.addElement("feature").addAttribute("var", "history-settings"); } // Check for HISTORY TRANSCRIPTS feature boolean historytranscriptsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("historytranscripts.enabled", "true")); if (historytranscriptsEnabled) { responseElement.addElement("feature").addAttribute("var", "history-transcripts"); } // Check for HOST NAME CHANGE feature boolean hostnameEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("hostname.enabled", "true")); if (hostnameEnabled) { responseElement.addElement("feature").addAttribute("var", "host-name"); } // Check for LOGIN AS INVISIBLE feature boolean invisibleloginEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("invisiblelogin.enabled", "true")); if (invisibleloginEnabled) { responseElement.addElement("feature").addAttribute("var", "invisible-login"); } // Check for LOGIN ANONYMOUSLY feature boolean anonymousloginEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("anonymouslogin.enabled", "true")); if (anonymousloginEnabled) { responseElement.addElement("feature").addAttribute("var", "anonymous-login"); } // Check for LOGOUT & EXIT feature boolean logoutexitEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("logoutexit.enabled", "true")); if (logoutexitEnabled) { responseElement.addElement("feature").addAttribute("var", "logout-exit"); } // Check for MOVE & COPY CONTACTS feature boolean movecopyEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("movecopy.enabled", "true")); if (movecopyEnabled) { responseElement.addElement("feature").addAttribute("var", "move-copy"); } // Check for MUC feature boolean mucEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("muc.enabled", "true")); if (mucEnabled) { responseElement.addElement("feature").addAttribute("var", "muc"); } // Check for PASSWORD CHANGE feature boolean passwordchangeEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("passwordchange.enabled", "true")); if (passwordchangeEnabled) { responseElement.addElement("feature").addAttribute("var", "password-change"); } // Check for PERSON SEARCH FIELD feature boolean personsearchEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("personsearch.enabled", "true")); if (personsearchEnabled) { responseElement.addElement("feature").addAttribute("var", "person-search"); } // Check for PLUGINS MENU feature boolean pluginsEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("plugins.enabled", "true")); if (pluginsEnabled) { responseElement.addElement("feature").addAttribute("var", "plugins-menu"); } // Check for PREFERENCES MENU feature boolean preferencesEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("preferences.enabled", "true")); if (preferencesEnabled) { responseElement.addElement("feature").addAttribute("var", "preferences-menu"); } // Check for PRESENCE STATUS CHANGE feature boolean presenceEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("presence.enabled", "true")); if (presenceEnabled) { responseElement.addElement("feature").addAttribute("var", "presence-status"); } // Check for PROFILE & AVATAR EDITING feature boolean vcardEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("vcard.enabled", "true")); if (vcardEnabled) { responseElement.addElement("feature").addAttribute("var", "vcard"); } // Check for SAVE PASSWORD & AUTOLOGIN feature boolean savepassandautologinEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("savepassandautologin.enabled", "true")); if (savepassandautologinEnabled) { responseElement.addElement("feature").addAttribute("var", "save-password"); } // Check for UPDATES feature boolean updatesEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("updates.enabled", "true")); if (updatesEnabled) { responseElement.addElement("feature").addAttribute("var", "updates"); } // Check for VIEW NOTES feature boolean viewnotesEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("viewnotes.enabled", "true")); if (viewnotesEnabled) { responseElement.addElement("feature").addAttribute("var", "view-notes"); } // Check for VIEW TASK LIST feature boolean viewtasklistEnabled = Boolean.parseBoolean(JiveGlobals.getProperty("viewtasklist.enabled", "true")); if (viewtasklistEnabled) { responseElement.addElement("feature").addAttribute("var", "view-tasks"); } } /** * Notify all users who have requested disco information from this component that settings have been changed. * Clients should perform a new service discovery to see what has changed. */ public void notifyDiscoInfoChanged() { final Message message = new Message(); message.setFrom(serviceName + "." + componentManager.getServerName()); Element child = message.addChildElement("event", "http://jabber.org/protocol/disco#info"); buildFeatureSet(child); sessionManager.broadcast(message); } /** * Listener to check all new client connections to validate against * the server side client validate scheme. */ private class SparkSessionListener implements SessionEventListener { /** * A new session was created. * * @param session the newly created session. */ public void sessionCreated(final Session session) { // Check to see if Spark is required. String clientsAllowed = JiveGlobals.getProperty("clients.allowed", "all"); final boolean disconnectIfNoMatch = !"all".equals(clientsAllowed); if (disconnectIfNoMatch) { // TODO: A future version may want to close sessions of users that never // TODO: responded the IQ version request. taskEngine.schedule(new TimerTask() { @Override public void run() { requestSoftwareVersion(session); } }, 5000); } } /** * A session was destroyed. * * @param session the session destroyed. */ public void sessionDestroyed(Session session) { } public void resourceBound(Session session) { // Do nothing. } public void anonymousSessionCreated(Session session) { // Ignore. } public void anonymousSessionDestroyed(Session session) { // Ignore. } } /** * Make a version request (JEP-0092) of the client the specified session is using. If the response is NOT the Spark IM * client, send a StreamError notification and disconnect the user. * * @param session the users session. */ private void requestSoftwareVersion(final Session session) { // Send IQ get to check client version. final IQ clientPacket = new IQ(IQ.Type.get); clientPacket.setTo(session.getAddress()); clientPacket.setFrom(serviceName + "." + componentManager.getServerName()); clientPacket.setChildElement("query", "jabber:iq:version"); sendPacket(clientPacket); } /** * Sends an unsupported version error and name of client the user attempted to connect with. * * @param session the users current session. * @param clientName the name of the client they were connecting with. */ private void closeSession(final Session session, String clientName) { // Increase the number of logins not allowed by 1. disconnects.incrementAndGet(); Log.debug("Closed connection to client attempting to connect from " + clientName); // Send message information user. final Message message = new Message(); message.setFrom(serviceName + "." + componentManager.getServerName()); message.setTo(session.getAddress()); message.setBody("You are using an invalid client, and therefore will be disconnected. " + "Please ask your system administrator for client choices."); // Send Message sendPacket(message); // Disconnect user after 5 seconds. taskEngine.schedule(new TimerTask() { @Override public void run() { // Include the not-authorized error in the response StreamError error = new StreamError(StreamError.Condition.policy_violation); session.deliverRawText(error.toXML()); // Close the underlying connection session.close(); } }, 5000); } /** * Returns the number of logins which were not valid due to Spark Manager restrictions. * * @return the number of logins not allowed. */ public int getNumberOfLoginsNotAllowed() { return disconnects.getAndSet(0); } }