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