aboutsummaryrefslogtreecommitdiffstats
path: root/libs/xmpp-addr/src/main/java/rocks/xmpp/addr/FullJid.java
blob: 24130fd1b3b20c4ec9521bff9311f7d132f5ec9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2017 Christian Schudt
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package rocks.xmpp.addr;

import rocks.xmpp.precis.PrecisProfile;
import rocks.xmpp.precis.PrecisProfiles;
import rocks.xmpp.util.cache.LruCache;

import java.net.IDN;
import java.nio.charset.Charset;
import java.text.Normalizer;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 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>.
 * <p>
 * This class is thread-safe and immutable.
 *
 * @author Christian Schudt
 * @see <a href="https://tools.ietf.org/html/rfc7622">RFC 7622 - Extensible Messaging and Presence Protocol (XMPP): Address Format</a>
 */
final class FullJid extends AbstractJid {

    /**
     * Escapes all disallowed characters and also backslash, when followed by a defined hex code for escaping. See 4. Business Rules.
     */
    private static final Pattern ESCAPE_PATTERN = Pattern.compile("[ \"&'/:<>@]|\\\\(?=20|22|26|27|2f|3a|3c|3e|40|5c)");

    private static final Pattern UNESCAPE_PATTERN = Pattern.compile("\\\\(20|22|26|27|2f|3a|3c|3e|40|5c)");

    private static final Pattern JID = Pattern.compile("^((.*?)@)?([^/@]+)(/(.*))?$");

    private static final IDNProfile IDN_PROFILE = new IDNProfile();

    /**
     * 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).
     */
    private static final String DOTS = "[.\u3002\uFF0E\uFF61]";

    /**
     * Label separators for domain labels, which should be mapped to "." (dot): IDEOGRAPHIC FULL STOP character (U+3002)
     */
    private static final Pattern LABEL_SEPARATOR = Pattern.compile(DOTS);

    private static final Pattern LABEL_SEPARATOR_FINAL = Pattern.compile(DOTS + "$");

    /**
     * Caches the escaped JIDs.
     */
    private static final Map<CharSequence, Jid> ESCAPED_CACHE = new LruCache<>(5000);

    /**
     * Caches the unescaped JIDs.
     */
    private static final Map<CharSequence, Jid> UNESCAPED_CACHE = new LruCache<>(5000);

    private static final long serialVersionUID = -3824234106101731424L;

    private final String escapedLocal;

    private final String local;

    private final String domain;

    private final String resource;

    private final Jid bareJid;

    /**
     * Creates a full JID with local, domain and resource part.
     *
     * @param local    The local part.
     * @param domain   The domain part.
     * @param resource The resource part.
     */
    FullJid(CharSequence local, CharSequence domain, CharSequence resource) {
        this(local, domain, resource, false, null);
    }

    private FullJid(final CharSequence local, final CharSequence domain, final CharSequence resource, final boolean doUnescape, Jid bareJid) {
        final String enforcedLocalPart;
        final String enforcedDomainPart;
        final String enforcedResource;

        final String unescapedLocalPart;

        if (domain == null) {
            throw new NullPointerException();
        }

        if (doUnescape) {
            unescapedLocalPart = unescape(local);
        } else {
            unescapedLocalPart = local != null ? local.toString() : null;
        }

        // Escape the local part, so that disallowed characters like the space characters pass the UsernameCaseMapped profile.
        final String escapedLocalPart = escape(unescapedLocalPart);

        // If the domainpart includes a final character considered to be a label
        // separator (dot) by [RFC1034], this character MUST be stripped from
        // the domainpart before the JID of which it is a part is used for the
        // purpose of routing an XML stanza, comparing against another JID, or
        // constructing an XMPP URI or IRI [RFC5122].  In particular, such a
        // character MUST be stripped before any other canonicalization steps
        // are taken.
        // Also validate, that the domain name can be converted to ASCII, i.e. validate the domain name (e.g. must not start with "_").
        final String strDomain = IDN.toASCII(LABEL_SEPARATOR_FINAL.matcher(domain).replaceAll(""), IDN.USE_STD3_ASCII_RULES);
        enforcedLocalPart = escapedLocalPart != null ? PrecisProfiles.USERNAME_CASE_MAPPED.enforce(escapedLocalPart) : null;
        enforcedResource = resource != null ? PrecisProfiles.OPAQUE_STRING.enforce(resource) : null;
        // See https://tools.ietf.org/html/rfc5895#section-2
        enforcedDomainPart = IDN_PROFILE.enforce(strDomain);

        validateLength(enforcedLocalPart, "local");
        validateLength(enforcedResource, "resource");
        validateDomain(strDomain);

        this.local = unescape(enforcedLocalPart);
        this.escapedLocal = enforcedLocalPart;
        this.domain = enforcedDomainPart;
        this.resource = enforcedResource;
        if (bareJid != null) {
            this.bareJid = bareJid;
        } else {
            this.bareJid = isBareJid() ? this : new AbstractJid() {

                @Override
                public Jid asBareJid() {
                    return this;
                }

                @Override
                public Jid withLocal(CharSequence local) {
                    if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
                        return this;
                    }
                    return new FullJid(local, getDomain(), getResource(), false, null);
                }

                @Override
                public Jid withResource(CharSequence resource) {
                    if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
                        return this;
                    }
                    return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
                }

                @Override
                public Jid atSubdomain(CharSequence subdomain) {
                    if (subdomain == null) {
                        throw new NullPointerException();
                    }
                    return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
                }

                @Override
                public String getLocal() {
                    return FullJid.this.getLocal();
                }

                @Override
                public String getEscapedLocal() {
                    return FullJid.this.getEscapedLocal();
                }

                @Override
                public String getDomain() {
                    return FullJid.this.getDomain();
                }

                @Override
                public String getResource() {
                    return null;
                }
            };
        }
    }

    /**
     * Creates a JID from a string. The format must be
     * <blockquote><p>[ localpart "@" ] domainpart [ "/" resourcepart ]</p></blockquote>
     *
     * @param jid        The JID.
     * @param doUnescape If the jid parameter will be unescaped.
     * @return The JID.
     * @throws NullPointerException     If the jid is null.
     * @throws IllegalArgumentException If the jid could not be parsed or is not valid.
     * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
     */
    static Jid of(String jid, final boolean doUnescape) {
        if (jid == null) {
            throw new NullPointerException("jid must not be null.");
        }

        jid = jid.trim();

        if (jid.isEmpty()) {
            throw new IllegalArgumentException("jid must not be empty.");
        }

        Jid result;
        if (doUnescape) {
            result = UNESCAPED_CACHE.get(jid);
        } else {
            result = ESCAPED_CACHE.get(jid);
        }

        if (result != null) {
            return result;
        }

        Matcher matcher = JID.matcher(jid);
        if (matcher.matches()) {
            Jid jidValue = new FullJid(matcher.group(2), matcher.group(3), matcher.group(5), doUnescape, null);
            if (doUnescape) {
                UNESCAPED_CACHE.put(jid, jidValue);
            } else {
                ESCAPED_CACHE.put(jid, jidValue);
            }
            return jidValue;
        } else {
            throw new IllegalArgumentException("Could not parse JID: " + jid);
        }
    }

    /**
     * Escapes a local part. The characters {@code "&'/:<>@} (+ whitespace) are replaced with their respective escape characters.
     *
     * @param localPart The local part.
     * @return The escaped local part or null.
     * @see <a href="https://xmpp.org/extensions/xep-0106.html">XEP-0106: JID Escaping</a>
     */
    private static String escape(final CharSequence localPart) {
        if (localPart != null) {
            final Matcher matcher = ESCAPE_PATTERN.matcher(localPart);
            final StringBuffer sb = new StringBuffer();
            while (matcher.find()) {
                matcher.appendReplacement(sb, "\\\\" + Integer.toHexString(matcher.group().charAt(0)));
            }
            matcher.appendTail(sb);
            return sb.toString();
        }
        return null;
    }

    private static String unescape(final CharSequence localPart) {
        if (localPart != null) {
            final Matcher matcher = UNESCAPE_PATTERN.matcher(localPart);
            final StringBuffer sb = new StringBuffer();
            while (matcher.find()) {
                final char c = (char) Integer.parseInt(matcher.group(1), 16);
                if (c == '\\') {
                    matcher.appendReplacement(sb, "\\\\");
                } else {
                    matcher.appendReplacement(sb, String.valueOf(c));
                }
            }
            matcher.appendTail(sb);
            return sb.toString();
        }
        return null;
    }

    private static void validateDomain(String domain) {
        if (domain == null) {
            throw new NullPointerException("domain must not be null.");
        }
        if (domain.contains("@")) {
            // Prevent misuse of API.
            throw new IllegalArgumentException("domain must not contain a '@' sign");
        }
        validateLength(domain, "domain");
    }

    /**
     * Validates that the length of a local, domain or resource part is not longer than 1023 characters.
     *
     * @param value The value.
     * @param part  The part, only used to produce an exception message.
     */
    private static void validateLength(CharSequence value, CharSequence part) {
        if (value != null) {
            if (value.length() == 0) {
                throw new IllegalArgumentException(part + " must not be empty.");
            }
            if (value.toString().getBytes(Charset.forName("UTF-8")).length > 1023) {
                throw new IllegalArgumentException(part + " must not be greater than 1023 bytes.");
            }
        }
    }

    /**
     * Converts this JID into a bare JID, i.e. removes the resource part.
     * <blockquote>
     * <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>
     * </blockquote>
     *
     * @return The bare JID.
     * @see #withResource(CharSequence)
     */
    @Override
    public final Jid asBareJid() {
        return bareJid;
    }

    /**
     * Gets the local part of the JID, also known as the name or node.
     * <blockquote>
     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.3">3.3.  Localpart</a></cite></p>
     * <p>The localpart of a JID is an optional identifier placed before the
     * domainpart and separated from the latter by the '@' character.
     * Typically, a localpart uniquely identifies the entity requesting and
     * using network access provided by a server (i.e., a local account),
     * although it can also represent other kinds of entities (e.g., a
     * chatroom associated with a multi-user chat service [XEP-0045]).  The
     * entity represented by an XMPP localpart is addressed within the
     * context of a specific domain (i.e., &lt;localpart@domainpart&gt;).</p>
     * </blockquote>
     *
     * @return The local part or null.
     */
    @Override
    public final String getLocal() {
        return local;
    }

    @Override
    public final String getEscapedLocal() {
        return escapedLocal;
    }

    /**
     * Gets the domain part.
     * <blockquote>
     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.2">3.2.  Domainpart</a></cite></p>
     * <p>The domainpart is the primary identifier and is the only REQUIRED
     * element of a JID (a mere domainpart is a valid JID).  Typically,
     * a domainpart identifies the "home" server to which clients connect
     * for XML routing and data management functionality.</p>
     * </blockquote>
     *
     * @return The domain part.
     */
    @Override
    public final String getDomain() {
        return domain;
    }

    /**
     * Gets the resource part.
     * <blockquote>
     * <p><cite><a href="https://tools.ietf.org/html/rfc7622#section-3.4">3.4.  Resourcepart</a></cite></p>
     * <p>The resourcepart of a JID is an optional identifier placed after the
     * domainpart and separated from the latter by the '/' character.  A
     * resourcepart can modify either a &lt;localpart@domainpart&gt; address or a
     * mere &lt;domainpart&gt; address.  Typically, a resourcepart uniquely
     * identifies a specific connection (e.g., a device or location) or
     * object (e.g., an occupant in a multi-user chatroom [XEP-0045])
     * belonging to the entity associated with an XMPP localpart at a domain
     * (i.e., &lt;localpart@domainpart/resourcepart&gt;).</p>
     * </blockquote>
     *
     * @return The resource part or null.
     */
    @Override
    public final String getResource() {
        return resource;
    }

    /**
     * Creates a new JID with a new local part and the same domain and resource part of the current JID.
     *
     * @param local The local part.
     * @return The JID with a new local part.
     * @throws IllegalArgumentException If the local is not a valid local part.
     * @see #withResource(CharSequence)
     */
    @Override
    public final Jid withLocal(CharSequence local) {
        if (local == this.getLocal() || local != null && local.equals(this.getLocal())) {
            return this;
        }
        return new FullJid(local, getDomain(), getResource(), false, null);
    }

    /**
     * Creates a new full JID with a resource and the same local and domain part of the current JID.
     *
     * @param resource The resource.
     * @return The full JID with a resource.
     * @throws IllegalArgumentException If the resource is not a valid resource part.
     * @see #asBareJid()
     * @see #withLocal(CharSequence)
     */
    @Override
    public final Jid withResource(CharSequence resource) {
        if (resource == this.getResource() || resource != null && resource.equals(this.getResource())) {
            return this;
        }
        return new FullJid(getLocal(), getDomain(), resource, false, asBareJid());
    }

    /**
     * Creates a new JID at a subdomain and at the same domain as this JID.
     *
     * @param subdomain The subdomain.
     * @return The JID at a subdomain.
     * @throws NullPointerException     If subdomain is null.
     * @throws IllegalArgumentException If subdomain is not a valid subdomain name.
     */
    @Override
    public final Jid atSubdomain(CharSequence subdomain) {
        if (subdomain != null) {
            throw new NullPointerException();
        }
        return new FullJid(getLocal(), subdomain + "." + getDomain(), getResource(), false, null);
    }

    /**
     * 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.
     *
     * @see <a href="https://tools.ietf.org/html/rfc5895#section-2">RFC 5895</a>
     */
    private static final class IDNProfile extends PrecisProfile {

        private IDNProfile() {
            super(false);
        }

        @Override
        public String prepare(CharSequence input) {
            return IDN.toUnicode(input.toString(), IDN.USE_STD3_ASCII_RULES);
        }

        @Override
        public String enforce(CharSequence input) {
            // 4. Map IDEOGRAPHIC FULL STOP character (U+3002) to dot.
            return applyAdditionalMappingRule(
                    // 3.  All characters are mapped using Unicode Normalization Form C (NFC).
                    applyNormalizationRule(
                            // 2. Fullwidth and halfwidth characters (those defined with
                            // Decomposition Types <wide> and <narrow>) are mapped to their
                            // decomposition mappings
                            applyWidthMappingRule(
                                    // 1. Uppercase characters are mapped to their lowercase equivalents
                                    applyCaseMappingRule(prepare(input))))).toString();
        }

        @Override
        protected CharSequence applyWidthMappingRule(CharSequence charSequence) {
            return widthMap(charSequence);
        }

        @Override
        protected CharSequence applyAdditionalMappingRule(CharSequence charSequence) {
            return LABEL_SEPARATOR.matcher(charSequence).replaceAll(".");
        }

        @Override
        protected CharSequence applyCaseMappingRule(CharSequence charSequence) {
            return charSequence.toString().toLowerCase();
        }

        @Override
        protected CharSequence applyNormalizationRule(CharSequence charSequence) {
            return Normalizer.normalize(charSequence, Normalizer.Form.NFC);
        }

        @Override
        protected CharSequence applyDirectionalityRule(CharSequence charSequence) {
            return charSequence;
        }
    }
}