FullJid.java

  1/*
  2 * The MIT License (MIT)
  3 *
  4 * Copyright (c) 2014-2017 Christian Schudt
  5 *
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 *
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 *
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24
 25package rocks.xmpp.addr;
 26
 27import rocks.xmpp.precis.PrecisProfile;
 28import rocks.xmpp.precis.PrecisProfiles;
 29import rocks.xmpp.util.cache.LruCache;
 30
 31import java.net.IDN;
 32import java.nio.charset.StandardCharsets;
 33import java.text.Normalizer;
 34import java.util.Map;
 35import java.util.Objects;
 36import java.util.regex.Matcher;
 37import java.util.regex.Pattern;
 38
 39/**
 40 * The implementation of the JID as described in <a href="https://tools.ietf.org/html/rfc7622">Extensible Messaging and Presence Protocol (XMPP): Address Format</a>.
 41 * <p>
 42 * This class is thread-safe and immutable.
 43 *
 44 * @author Christian Schudt
 45 * @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
 46 */
 47final class FullJid extends AbstractJid {
 48
 49    /**
 50     * Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules.
 51     */
 52    private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)");
 53
 54    private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)");
 55
 56    private static final Pattern JID = Pattern.compile("^((.*?)@)?([^/@]+)(/(.*))?$");
 57
 58    private static final IDNProfile IDN_PROFILE = new IDNProfile();
 59
 60    /**
 61     * Whenever dots are used as label separators, the following characters MUST be recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 (halfwidth ideographic full stop).
 62     */
 63    private static final String DOTS = "[.\u3002\uFF0E\uFF61]";
 64
 65    /**
 66     * Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002)
 67     */
 68    private static final Pattern LABEL_SEPARATOR = Pattern.compile(DOTS);
 69
 70    private static final Pattern LABEL_SEPARATOR_FINAL = Pattern.compile(DOTS + "$");
 71
 72    /**
 73     * Caches the escaped JIDs.
 74     */
 75    private static final Map<CharSequence, Jid> ESCAPED_CACHE = new LruCache<>(5000);
 76
 77    /**
 78     * Caches the unescaped JIDs.
 79     */
 80    private static final Map<CharSequence, Jid> UNESCAPED_CACHE = new LruCache<>(5000);
 81
 82    private static final long serialVersionUID = -3824234106101731424L;
 83
 84    private final String escapedLocal;
 85
 86    private final String local;
 87
 88    private final String domain;
 89
 90    private final String resource;
 91
 92    private final Jid bareJid;
 93
 94    /**
 95     * Creates a full JID with local, domain and resource part.
 96     *
 97     * @param local    The local part.
 98     * @param domain   The domain part.
 99     * @param resource The resource part.
100     */
101    FullJid(CharSequence local, CharSequence domain, CharSequence resource) {
102        this(local, domain, resource, false, null);
103    }
104
105    private FullJid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, Jid bareJid) {
106        final String enforcedLocalPart;
107        final String enforcedDomainPart;
108        final String enforcedResource;
109
110        final String unescapedLocalPart;
111
112        if (doUnescape) {
113            unescapedLocalPart = unescape(local);
114        } else {
115            unescapedLocalPart = local != null ? local.toString() : null;
116        }
117
118        // Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile.
119        final String escapedLocalPart = escape(unescapedLocalPart);
120
121        // If the domainpart includes a final character considered to be a label
122        // separator (dot) by [RFC1034], this character MUST be stripped from
123        // the domainpart before the JID of which it is a part is used for the
124        // purpose of routing an XML stanza, comparing against another JID, or
125        // constructing an XMPP URI or IRI [RFC5122].  In particular, such a
126        // character MUST be stripped before any other canonicalization steps
127        // are taken.
128        // Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_").
129        final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(Objects.requireNonNull(domain)).replaceAll(""), IDN.USE_STD3_ASCII_RULES);
130        enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null;
131        enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null;
132        // See https://tools.ietf.org/html/rfc5895#section-2
133        enforcedDomainPart = IDN_PROFILE.enforce(strDomain);
134
135        validateLength(enforcedLocalPart, "local");
136        validateLength(enforcedResource, "resource");
137        validateDomain(strDomain);
138
139        this.local = unescape(enforcedLocalPart);
140        this.escapedLocal = enforcedLocalPart;
141        this.domain = enforcedDomainPart;
142        this.resource = enforcedResource;
143        if (bareJid != null) {
144            this.bareJid = bareJid;
145        } else {
146            this.bareJid = isBareJid() ? this : new AbstractJid() {
147
148                @Override
149                public Jid asBareJid() {
150                    return this;
151                }
152
153                @Override
154                public Jid withLocal(CharSequence local) {
155                    if (Objects.equals(local, this.getLocal())) {
156                        return this;
157                    }
158                    return new FullJid(local, getDomain(), getResource(), false, null);
159                }
160
161                @Override
162                public Jid withResource(CharSequence resource) {
163                    if (Objects.equals(resource, this.getResource())) {
164                        return this;
165                    }
166                    return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
167                }
168
169                @Override
170                public Jid atSubdomain(CharSequence subdomain) {
171                    return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null);
172                }
173
174                @Override
175                public String getLocal() {
176                    return FullJid.this.getLocal();
177                }
178
179                @Override
180                public String getEscapedLocal() {
181                    return FullJid.this.getEscapedLocal();
182                }
183
184                @Override
185                public String getDomain() {
186                    return FullJid.this.getDomain();
187                }
188
189                @Override
190                public String getResource() {
191                    return null;
192                }
193            };
194        }
195    }
196
197    /**
198     * Creates a JID from a string. The format must be
199     * <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
200     *
201     * @param jid        The JID.
202     * @param doUnescape If the jid parameter will be unescaped.
203     * @return The JID.
204     * @throws NullPointerException     If the jid is null.
205     * @throws IllegalArgumentException If the jid could not be parsed or is not valid.
206     * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
207     */
208    static Jid of(String jid, final boolean doUnescape) {
209        Objects.requireNonNull(jid, "jid must not be null.");
210
211        jid = jid.trim();
212
213        if (jid.isEmpty()) {
214            throw new IllegalArgumentException("jid must not be empty.");
215        }
216
217        Jid result;
218        if (doUnescape) {
219            result = UNESCAPED_CACHE.get(jid);
220        } else {
221            result = ESCAPED_CACHE.get(jid);
222        }
223
224        if (result != null) {
225            return result;
226        }
227
228        Matcher matcher = JID.matcher(jid);
229        if (matcher.matches()) {
230            Jid jidValue = new FullJid(matcher.group(2), matcher.group(3), matcher.group(5), doUnescape, null);
231            if (doUnescape) {
232                UNESCAPED_CACHE.put(jid, jidValue);
233            } else {
234                ESCAPED_CACHE.put(jid, jidValue);
235            }
236            return jidValue;
237        } else {
238            throw new IllegalArgumentException("Could not parse JID: " + jid);
239        }
240    }
241
242    /**
243     * Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters.
244     *
245     * @param localPart The local part.
246     * @return The escaped local part or null.
247     * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
248     */
249    private static String escape(final CharSequence localPart) {
250        if (localPart != null) {
251            final Matcher matcher = ESCAPE_PATTERN.matcher(localPart);
252            final StringBuffer sb = new StringBuffer();
253            while (matcher.find()) {
254                matcher.appendReplacement(sb, "\\\\" + Integer.toHexString(matcher.group().charAt(0)));
255            }
256            matcher.appendTail(sb);
257            return sb.toString();
258        }
259        return null;
260    }
261
262    private static String unescape(final CharSequence localPart) {
263        if (localPart != null) {
264            final Matcher matcher = UNESCAPE_PATTERN.matcher(localPart);
265            final StringBuffer sb = new StringBuffer();
266            while (matcher.find()) {
267                final char c = (char) Integer.parseInt(matcher.group(1), 16);
268                if (c == '\\') {
269                    matcher.appendReplacement(sb, "\\\\");
270                } else {
271                    matcher.appendReplacement(sb, String.valueOf(c));
272                }
273            }
274            matcher.appendTail(sb);
275            return sb.toString();
276        }
277        return null;
278    }
279
280    private static void validateDomain(String domain) {
281        Objects.requireNonNull(domain, "domain must not be null.");
282        if (domain.contains("@")) {
283            // Prevent misuse of API.
284            throw new IllegalArgumentException("domain must not contain a '@' sign");
285        }
286        validateLength(domain, "domain");
287    }
288
289    /**
290     * Validates that the length of a local, domain or resource part is not longer than 1023 characters.
291     *
292     * @param value The value.
293     * @param part  The part, only used to produce an exception message.
294     */
295    private static void validateLength(CharSequence value, CharSequence part) {
296        if (value != null) {
297            if (value.length() == 0) {
298                throw new IllegalArgumentException(part + " must not be empty.");
299            }
300            if (value.toString().getBytes(StandardCharsets.UTF_8).length > 1023) {
301                throw new IllegalArgumentException(part + " must not be greater than 1023 bytes.");
302            }
303        }
304    }
305
306    /**
307     * Converts this JID into a bare JID, i.e. removes the resource part.
308     * <blockquote>
309     * <p>The term "bare JID" refers to an XMPP address of the form &lt;localpart@domainpart&gt; (for an account at a server) or of the form &lt;domainpart&gt; (for a server).</p>
310     * </blockquote>
311     *
312     * @return The bare JID.
313     * @see #withResource(CharSequence)
314     */
315    @Override
316    public final Jid asBareJid() {
317        return bareJid;
318    }
319
320    /**
321     * Gets the local part of the JID, also known as the name or node.
322     * <blockquote>
323     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3.  Localpart</a></cite></p>
324     * <p>The localpart of a JID is an optional identifier placed before the
325     * domainpart and separated from the latter by the '@' character.
326     * Typically, a localpart uniquely identifies the entity requesting and
327     * using network access provided by a server (i.e., a local account),
328     * although it can also represent other kinds of entities (e.g., a
329     * chatroom associated with a multi-user chat service [XEP-0045]).  The
330     * entity represented by an XMPP localpart is addressed within the
331     * context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
332     * </blockquote>
333     *
334     * @return The local part or null.
335     */
336    @Override
337    public final String getLocal() {
338        return local;
339    }
340
341    @Override
342    public final String getEscapedLocal() {
343        return escapedLocal;
344    }
345
346    /**
347     * Gets the domain part.
348     * <blockquote>
349     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2.  Domainpart</a></cite></p>
350     * <p>The domainpart is the primary identifier and is the only REQUIRED
351     * element of a JID (a mere domainpart is a valid JID).  Typically,
352     * a domainpart identifies the "home" server to which clients connect
353     * for XML routing and data management functionality.</p>
354     * </blockquote>
355     *
356     * @return The domain part.
357     */
358    @Override
359    public final String getDomain() {
360        return domain;
361    }
362
363    /**
364     * Gets the resource part.
365     * <blockquote>
366     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4.  Resourcepart</a></cite></p>
367     * <p>The resourcepart of a JID is an optional identifier placed after the
368     * domainpart and separated from the latter by the '/' character.  A
369     * resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
370     * mere &lt;domainpart&gt; address.  Typically, a resourcepart uniquely
371     * identifies a specific connection (e.g., a device or location) or
372     * object (e.g., an occupant in a multi-user chatroom [XEP-0045])
373     * belonging to the entity associated with an XMPP localpart at a domain
374     * (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
375     * </blockquote>
376     *
377     * @return The resource part or null.
378     */
379    @Override
380    public final String getResource() {
381        return resource;
382    }
383
384    /**
385     * Creates a new JID with a new local part and the same domain and resource part of the current JID.
386     *
387     * @param local The local part.
388     * @return The JID with a new local part.
389     * @throws IllegalArgumentException If the local is not a valid local part.
390     * @see #withResource(CharSequence)
391     */
392    @Override
393    public final Jid withLocal(CharSequence local) {
394        if (Objects.equals(local, this.getLocal())) {
395            return this;
396        }
397        return new FullJid(local, getDomain(), getResource(), false, null);
398    }
399
400    /**
401     * Creates a new full JID with a resource and the same local and domain part of the current JID.
402     *
403     * @param resource The resource.
404     * @return The full JID with a resource.
405     * @throws IllegalArgumentException If the resource is not a valid resource part.
406     * @see #asBareJid()
407     * @see #withLocal(CharSequence)
408     */
409    @Override
410    public final Jid withResource(CharSequence resource) {
411        if (Objects.equals(resource, this.getResource())) {
412            return this;
413        }
414        return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
415    }
416
417    /**
418     * Creates a new JID at a subdomain and at the same domain as this JID.
419     *
420     * @param subdomain The subdomain.
421     * @return The JID at a subdomain.
422     * @throws NullPointerException     If subdomain is null.
423     * @throws IllegalArgumentException If subdomain is not a valid subdomain name.
424     */
425    @Override
426    public final Jid atSubdomain(CharSequence subdomain) {
427        return new FullJid(getLocal(), Objects.requireNonNull(subdomain) + "." + getDomain(), getResource(), false, null);
428    }
429
430    /**
431     * A profile for applying the rules for IDN as in RFC 5895. Although IDN doesn't use Precis, it's still very similar so that we can use the base class.
432     *
433     * @see <a href="https://tools.ietf.org/html/rfc5895#section-2">RFC 5895</a>
434     */
435    private static final class IDNProfile extends PrecisProfile {
436
437        private IDNProfile() {
438            super(false);
439        }
440
441        @Override
442        public String prepare(CharSequence input) {
443            return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES);
444        }
445
446        @Override
447        public String enforce(CharSequence input) {
448            // 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot.
449            return applyAdditionalMappingRule(
450                    // 3.  All characters are mapped using Unicode Normalization Form C (NFC).
451                    applyNormalizationRule(
452                            // 2. Fullwidth and halfwidth characters (those defined with
453                            // Decomposition Types <wide> and <narrow>) are mapped to their
454                            // decomposition mappings
455                            applyWidthMappingRule(
456                                    // 1. Uppercase characters are mapped to their lowercase equivalents
457                                    applyCaseMappingRule(prepare(input))))).toString();
458        }
459
460        @Override
461        protected CharSequence applyWidthMappingRule(CharSequence charSequence) {
462            return widthMap(charSequence);
463        }
464
465        @Override
466        protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) {
467            return LABEL_SEPARATOR.matcher(charSequence).replaceAll(".");
468        }
469
470        @Override
471        protected CharSequence applyCaseMappingRule(CharSequence charSequence) {
472            return charSequence.toString().toLowerCase();
473        }
474
475        @Override
476        protected CharSequence applyNormalizationRule(CharSequence charSequence) {
477            return Normalizer.normalize(charSequence, Normalizer.Form.NFC);
478        }
479
480        @Override
481        protected CharSequence applyDirectionalityRule(CharSequence charSequence) {
482            return charSequence;
483        }
484    }
485}