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