package org.jivesoftware.openfire.group; import java.nio.charset.StandardCharsets; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; /** * This class is designed to identify and manage custom JIDs * that represent Groups (rather than Users or Components). * * The node for a GroupJID is the group name encoded as base32hex. * This allows us to preserve special characters and upper/lower casing * within the group name. The encoded group name is valid according to * the RFC6122 rules for a valid node and does not require further * JID escaping. * * We use an MD5 hash of the group name as the resource value to help * distinguish Group JIDs from regular JIDs in the local domain when * they are persisted in the DB or over the network. * * @author Tom Evans * */ public class GroupJID extends JID { private static final Logger Log = LoggerFactory.getLogger(GroupJID.class); private static final long serialVersionUID = 5681300465012974014L; private transient String groupName; /** * Construct a JID representing a Group. * * @param name A group name for the local domain */ public GroupJID(String name) { super(encodeNode(name), XMPPServer.getInstance().getServerInfo().getXMPPDomain(), StringUtils.hash(name), true); groupName = name; } /** * Construct a JID representing a Group from a regular JID. This constructor is * private because it is used only from within this class after the source JID * has been validated. * * @param source A full JID representing a group * @see GroupJID#fromString */ private GroupJID(JID source) { // skip stringprep for the new group JID, since it has already been parsed super(source.getNode(), source.getDomain(), source.getResource(), true); } /** * Returns the group name corresponding to this JID. * * @return The name for the corresponding group */ public String getGroupName() { // lazy instantiation if (groupName == null) { groupName = decodeNode(getNode()); } return groupName; } /** * Override the base class implementation to retain the resource * identifier for group JIDs. * * @return This JID, as a group JID */ @Override public JID asBareJID() { return this; } /** * Override the base class implementation to retain the resource * identifier for group JIDs. * * @return The full JID rendered as a string */ @Override public String toBareJID() { return this.toString(); } @Override public int compareTo(JID jid) { // Comparison order is domain, node, resource. int compare = getDomain().compareTo(jid.getDomain()); if (compare == 0) { String otherNode = jid.getNode(); compare = otherNode == null ? 1 : getGroupName().compareTo(otherNode); } if (compare == 0) { compare = jid.getResource() == null ? 0 : -1; } return compare; } /** * Encode the given group name in base32hex (UTF-8). This encoding * is valid according to the nodeprep profile of stringprep * (RFC6122, Appendix A) and needs no further escaping. * * @param name A group name * @return The encoded group name */ private static String encodeNode(String name) { return StringUtils.encodeBase32(name); } /** * Decode the given group name from base32hex (UTF-8). * * @param name A group name, encoded as base32hex * @return The group name */ private static String decodeNode(String node) { return new String(StringUtils.decodeBase32(node), StandardCharsets.UTF_8); } /** * Check a JID to determine whether it represents a group. If the given * JID is an instance of this class, it is a group JID. Otherwise, * calculate the hash to determine whether the JID can be resolved to * a group. * * @param jid A JID, possibly representing a group * @return true if the given jid represents a group in the local domain */ public static boolean isGroup(JID jid) { try { return isGroup(jid, false); } catch (GroupNotFoundException gnfe) { // should not happen because we do not validate the group exists Log.error("Unexpected group validation", gnfe); return false; } } /** * Check a JID to determine whether it represents a group. If the given * JID is an instance of this class, it is a group JID. Otherwise, * calculate the hash to determine whether the JID can be resolved to * a group. This method also optionally validates that the corresponding * group actually exists in the local domain. * * @param jid A JID, possibly representing a group * @param groupMustExist If true, validate that the corresponding group actually exists * @return true if the given jid represents a group in the local domain * @throws GroupNotFoundException The JID represents a group, but the group does not exist */ public static boolean isGroup(JID jid, boolean groupMustExist) throws GroupNotFoundException { boolean isGroup = false; String groupName = null, node = jid.getNode(); if (node != null) { isGroup = (jid instanceof GroupJID) ? true : jid.getResource() != null && StringUtils.isBase32(node) && StringUtils.hash(groupName = decodeNode(node)).equals(jid.getResource()); if (isGroup && groupMustExist) { Log.debug("Validating group: " + jid); if (XMPPServer.getInstance().isLocal(jid)) { GroupManager.getInstance().getGroup(groupName); } else { isGroup = false; // not in the local domain } } } return isGroup; } /** * Returns a JID from the given JID. If the JID represents a group, * returns an instance of this class. Otherwise returns the given JID. * * @param jid A JID, possibly representing a group * @return A new GroupJID if the given JID represents a group, or the given JID */ public static JID fromJID(JID jid) { if (jid instanceof GroupJID || jid.getResource() == null || jid.getNode() == null) { return jid; } else { return (isGroup(jid)) ? new GroupJID(jid) : jid; } } /** * Creates a JID from the given string. If the string represents a group, * return an instance of this class. Otherwise returns a regular JID. * * @param jid A JID, possibly representing a group * @return A JID with a type appropriate to its content * @throws IllegalArgumentException the given string is not a valid JID */ public static JID fromString(String jid) { Log.debug("Parsing JID from string: " + jid); return fromJID(new JID(jid)); } }