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 <localpart@domainpart> (for an account at a server) or of the form <domainpart> (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., <localpart@domainpart>).</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 <localpart@domainpart> address or a
370 * mere <domainpart> 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., <localpart@domainpart/resourcepart>).</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}