1 package org.jivesoftware.openfire.group; 2 3 import java.nio.charset.StandardCharsets; 4 5 import org.jivesoftware.openfire.XMPPServer; 6 import org.jivesoftware.util.StringUtils; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 import org.xmpp.packet.JID; 10 11 /** 12 * This class is designed to identify and manage custom JIDs 13 * that represent Groups (rather than Users or Components). 14 * 15 * The node for a GroupJID is the group name encoded as base32hex. 16 * This allows us to preserve special characters and upper/lower casing 17 * within the group name. The encoded group name is valid according to 18 * the RFC6122 rules for a valid node and does not require further 19 * JID escaping. 20 * 21 * We use an MD5 hash of the group name as the resource value to help 22 * distinguish Group JIDs from regular JIDs in the local domain when 23 * they are persisted in the DB or over the network. 24 * 25 * @author Tom Evans 26 * 27 */ 28 public class GroupJID extends JID { 29 30 private static final Logger Log = LoggerFactory.getLogger(GroupJID.class); 31 private static final long serialVersionUID = 5681300465012974014L; 32 33 private transient String groupName; 34 35 /** 36 * Construct a JID representing a Group. 37 * 38 * @param name A group name for the local domain 39 */ GroupJID(String name)40 public GroupJID(String name) { 41 super(encodeNode(name), 42 XMPPServer.getInstance().getServerInfo().getXMPPDomain(), 43 StringUtils.hash(name), 44 true); 45 groupName = name; 46 } 47 48 /** 49 * Construct a JID representing a Group from a regular JID. This constructor is 50 * private because it is used only from within this class after the source JID 51 * has been validated. 52 * 53 * @param source A full JID representing a group 54 * @see GroupJID#fromString 55 */ GroupJID(JID source)56 private GroupJID(JID source) { 57 // skip stringprep for the new group JID, since it has already been parsed 58 super(source.getNode(), source.getDomain(), source.getResource(), true); 59 } 60 61 /** 62 * Returns the group name corresponding to this JID. 63 * 64 * @return The name for the corresponding group 65 */ getGroupName()66 public String getGroupName() { 67 // lazy instantiation 68 if (groupName == null) { 69 groupName = decodeNode(getNode()); 70 } 71 return groupName; 72 } 73 74 /** 75 * Override the base class implementation to retain the resource 76 * identifier for group JIDs. 77 * 78 * @return This JID, as a group JID 79 */ 80 @Override asBareJID()81 public JID asBareJID() { 82 return this; 83 } 84 85 /** 86 * Override the base class implementation to retain the resource 87 * identifier for group JIDs. 88 * 89 * @return The full JID rendered as a string 90 */ 91 @Override toBareJID()92 public String toBareJID() { 93 return this.toString(); 94 } 95 96 @Override compareTo(JID jid)97 public int compareTo(JID jid) { 98 // Comparison order is domain, node, resource. 99 int compare = getDomain().compareTo(jid.getDomain()); 100 if (compare == 0) { 101 String otherNode = jid.getNode(); 102 compare = otherNode == null ? 1 : getGroupName().compareTo(otherNode); 103 } 104 if (compare == 0) { 105 compare = jid.getResource() == null ? 0 : -1; 106 } 107 return compare; 108 } 109 110 111 /** 112 * Encode the given group name in base32hex (UTF-8). This encoding 113 * is valid according to the nodeprep profile of stringprep 114 * (RFC6122, Appendix A) and needs no further escaping. 115 * 116 * @param name A group name 117 * @return The encoded group name 118 */ encodeNode(String name)119 private static String encodeNode(String name) { 120 return StringUtils.encodeBase32(name); 121 } 122 123 /** 124 * Decode the given group name from base32hex (UTF-8). 125 * 126 * @param name A group name, encoded as base32hex 127 * @return The group name 128 */ decodeNode(String node)129 private static String decodeNode(String node) { 130 return new String(StringUtils.decodeBase32(node), StandardCharsets.UTF_8); 131 } 132 133 /** 134 * Check a JID to determine whether it represents a group. If the given 135 * JID is an instance of this class, it is a group JID. Otherwise, 136 * calculate the hash to determine whether the JID can be resolved to 137 * a group. 138 * 139 * @param jid A JID, possibly representing a group 140 * @return true if the given jid represents a group in the local domain 141 */ isGroup(JID jid)142 public static boolean isGroup(JID jid) { 143 try { 144 return isGroup(jid, false); 145 } catch (GroupNotFoundException gnfe) { 146 // should not happen because we do not validate the group exists 147 Log.error("Unexpected group validation", gnfe); 148 return false; 149 } 150 } 151 152 /** 153 * Check a JID to determine whether it represents a group. If the given 154 * JID is an instance of this class, it is a group JID. Otherwise, 155 * calculate the hash to determine whether the JID can be resolved to 156 * a group. This method also optionally validates that the corresponding 157 * group actually exists in the local domain. 158 * 159 * @param jid A JID, possibly representing a group 160 * @param groupMustExist If true, validate that the corresponding group actually exists 161 * @return true if the given jid represents a group in the local domain 162 * @throws GroupNotFoundException The JID represents a group, but the group does not exist 163 */ isGroup(JID jid, boolean groupMustExist)164 public static boolean isGroup(JID jid, boolean groupMustExist) throws GroupNotFoundException { 165 boolean isGroup = false; 166 String groupName = null, node = jid.getNode(); 167 if (node != null) { 168 169 isGroup = (jid instanceof GroupJID) ? true : 170 jid.getResource() != null && 171 StringUtils.isBase32(node) && 172 StringUtils.hash(groupName = decodeNode(node)).equals(jid.getResource()); 173 174 if (isGroup && groupMustExist) { 175 Log.debug("Validating group: " + jid); 176 if (XMPPServer.getInstance().isLocal(jid)) { 177 GroupManager.getInstance().getGroup(groupName); 178 } else { 179 isGroup = false; // not in the local domain 180 } 181 } 182 } 183 return isGroup; 184 } 185 186 /** 187 * Returns a JID from the given JID. If the JID represents a group, 188 * returns an instance of this class. Otherwise returns the given JID. 189 * 190 * @param jid A JID, possibly representing a group 191 * @return A new GroupJID if the given JID represents a group, or the given JID 192 */ fromJID(JID jid)193 public static JID fromJID(JID jid) { 194 if (jid instanceof GroupJID || jid.getResource() == null || jid.getNode() == null) { 195 return jid; 196 } else { 197 return (isGroup(jid)) ? new GroupJID(jid) : jid; 198 } 199 } 200 201 /** 202 * Creates a JID from the given string. If the string represents a group, 203 * return an instance of this class. Otherwise returns a regular JID. 204 * 205 * @param jid A JID, possibly representing a group 206 * @return A JID with a type appropriate to its content 207 * @throws IllegalArgumentException the given string is not a valid JID 208 */ fromString(String jid)209 public static JID fromString(String jid) { 210 Log.debug("Parsing JID from string: " + jid); 211 return fromJID(new JID(jid)); 212 } 213 214 } 215