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