1package eu.siacs.conversations.xmpp;
2
3import androidx.annotation.NonNull;
4import eu.siacs.conversations.utils.IP;
5import im.conversations.android.xmpp.model.stanza.Stanza;
6import java.io.Serializable;
7import java.util.regex.Pattern;
8import org.jxmpp.jid.impl.JidCreate;
9import org.jxmpp.jid.parts.Domainpart;
10import org.jxmpp.jid.parts.Localpart;
11import org.jxmpp.jid.parts.Resourcepart;
12import org.jxmpp.stringprep.XmppStringprepException;
13
14public abstract class Jid implements Comparable<Jid>, Serializable, CharSequence {
15
16 private static final Pattern HOSTNAME_PATTERN =
17 Pattern.compile(
18 "^(?=.{1,253}$)(?=.{1,253}$)(?!-)(?!.*--)(?!.*-$)[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$");
19
20 public static Jid of(
21 final CharSequence local, final CharSequence domain, final CharSequence resource) {
22 if (local == null) {
23 if (resource == null) {
24 return ofDomain(domain);
25 } else {
26 return ofDomainAndResource(domain, resource);
27 }
28 }
29 if (resource == null) {
30 return ofLocalAndDomain(local, domain);
31 }
32 try {
33 return new InternalRepresentation(
34 JidCreate.entityFullFrom(
35 Localpart.from(local.toString()),
36 Domainpart.from(domain.toString()),
37 Resourcepart.from(resource.toString())));
38 } catch (final XmppStringprepException e) {
39 throw new IllegalArgumentException(e);
40 }
41 }
42
43 public static Jid ofDomain(final CharSequence domain) {
44 try {
45 return new InternalRepresentation(JidCreate.domainBareFrom(domain));
46 } catch (final XmppStringprepException e) {
47 throw new IllegalArgumentException(e);
48 }
49 }
50
51 public static Jid ofLocalAndDomain(final CharSequence local, final CharSequence domain) {
52 try {
53 return new InternalRepresentation(
54 JidCreate.bareFrom(
55 Localpart.from(local.toString()), Domainpart.from(domain.toString())));
56 } catch (final XmppStringprepException e) {
57 throw new IllegalArgumentException(e);
58 }
59 }
60
61 public static Jid ofDomainAndResource(CharSequence domain, CharSequence resource) {
62 try {
63 return new InternalRepresentation(
64 JidCreate.domainFullFrom(
65 Domainpart.from(domain.toString()),
66 Resourcepart.from(resource.toString())));
67 } catch (final XmppStringprepException e) {
68 throw new IllegalArgumentException(e);
69 }
70 }
71
72 public static Jid of(final CharSequence input) {
73 if (input instanceof Jid jid) {
74 return jid;
75 }
76 try {
77 return new InternalRepresentation(JidCreate.from(input));
78 } catch (final XmppStringprepException e) {
79 throw new IllegalArgumentException(e);
80 }
81 }
82
83 public static Jid ofUserInput(final CharSequence input) {
84 final var jid = of(input);
85 final var domain = jid.getDomain().toString();
86 if (domain.isEmpty()) {
87 throw new IllegalArgumentException("Domain can not be empty");
88 }
89 if (HOSTNAME_PATTERN.matcher(domain).matches() || IP.matches(domain)) {
90 return jid;
91 }
92 throw new IllegalArgumentException("Invalid hostname");
93 }
94
95 public static Jid ofOrInvalid(final String input) {
96 return ofOrInvalid(input, false);
97 }
98
99 /**
100 * @param jid a string representation of the jid to parse
101 * @param fallback indicates whether an attempt should be made to parse a bare version of the
102 * jid
103 * @return an instance of Jid; may be Jid.Invalid
104 */
105 public static Jid ofOrInvalid(final String jid, final boolean fallback) {
106 try {
107 return Jid.of(jid);
108 } catch (final IllegalArgumentException e) {
109 return Jid.invalidOf(jid, fallback);
110 }
111 }
112
113 private static Jid invalidOf(final String jid, boolean fallback) {
114 final int pos = jid.indexOf('/');
115 if (fallback && pos >= 0 && jid.length() >= pos + 1) {
116 if (jid.substring(pos + 1).trim().isEmpty()) {
117 return Jid.of(jid.substring(0, pos));
118 }
119 }
120 return new Invalid(jid);
121 }
122
123 public abstract boolean isFullJid();
124
125 public abstract boolean isBareJid();
126
127 public abstract boolean isDomainJid();
128
129 public abstract Jid asBareJid();
130
131 public abstract Jid withResource(CharSequence resource);
132
133 public abstract String getLocal();
134
135 public abstract Jid getDomain();
136
137 public abstract String getResource();
138
139 private static class InternalRepresentation extends Jid {
140 private final org.jxmpp.jid.Jid inner;
141
142 private InternalRepresentation(final org.jxmpp.jid.Jid inner) {
143 this.inner = inner;
144 }
145
146 @Override
147 public boolean isFullJid() {
148 return inner.isEntityFullJid() || inner.isDomainFullJid();
149 }
150
151 @Override
152 public boolean isBareJid() {
153 return inner.isDomainBareJid() || inner.isEntityBareJid();
154 }
155
156 @Override
157 public boolean isDomainJid() {
158 return inner.isDomainBareJid() || inner.isDomainFullJid();
159 }
160
161 @Override
162 public Jid asBareJid() {
163 return new InternalRepresentation(inner.asBareJid());
164 }
165
166 @Override
167 public Jid withResource(CharSequence resource) {
168 final Localpart localpart = inner.getLocalpartOrNull();
169 try {
170 final Resourcepart resourcepart = Resourcepart.from(resource.toString());
171 if (localpart == null) {
172 return new InternalRepresentation(
173 JidCreate.domainFullFrom(inner.getDomain(), resourcepart));
174 } else {
175 return new InternalRepresentation(
176 JidCreate.fullFrom(localpart, inner.getDomain(), resourcepart));
177 }
178 } catch (XmppStringprepException e) {
179 throw new IllegalArgumentException(e);
180 }
181 }
182
183 @Override
184 public String getLocal() {
185 final Localpart localpart = inner.getLocalpartOrNull();
186 return localpart == null ? null : localpart.toString();
187 }
188
189 @Override
190 public Jid getDomain() {
191 return new InternalRepresentation(inner.asDomainBareJid());
192 }
193
194 @Override
195 public String getResource() {
196 final Resourcepart resourcepart = inner.getResourceOrNull();
197 return resourcepart == null ? null : resourcepart.toString();
198 }
199
200 @NonNull
201 @Override
202 public String toString() {
203 return inner.toString();
204 }
205
206 @Override
207 public int length() {
208 return inner.length();
209 }
210
211 @Override
212 public char charAt(int i) {
213 return inner.charAt(i);
214 }
215
216 @NonNull
217 @Override
218 public CharSequence subSequence(int i, int i1) {
219 return inner.subSequence(i, i1);
220 }
221
222 @Override
223 public int compareTo(Jid jid) {
224 if (jid instanceof InternalRepresentation) {
225 return inner.compareTo(((InternalRepresentation) jid).inner);
226 } else {
227 return 0;
228 }
229 }
230
231 @Override
232 public boolean equals(Object o) {
233 if (this == o) return true;
234 if (o == null || getClass() != o.getClass()) return false;
235 InternalRepresentation that = (InternalRepresentation) o;
236 return inner.equals(that.inner);
237 }
238
239 @Override
240 public int hashCode() {
241 return inner.hashCode();
242 }
243 }
244
245 public static class Invalid extends Jid {
246
247 private final String value;
248
249 private Invalid(final String jid) {
250 this.value = jid;
251 }
252
253 @Override
254 @NonNull
255 public String toString() {
256 return value;
257 }
258
259 @Override
260 public boolean isFullJid() {
261 throw new AssertionError("Not implemented");
262 }
263
264 @Override
265 public boolean isBareJid() {
266 throw new AssertionError("Not implemented");
267 }
268
269 @Override
270 public boolean isDomainJid() {
271 throw new AssertionError("Not implemented");
272 }
273
274 @Override
275 public Jid asBareJid() {
276 throw new AssertionError("Not implemented");
277 }
278
279 @Override
280 public Jid withResource(CharSequence charSequence) {
281 throw new AssertionError("Not implemented");
282 }
283
284 @Override
285 public String getLocal() {
286 throw new AssertionError("Not implemented");
287 }
288
289 @Override
290 public Jid getDomain() {
291 throw new AssertionError("Not implemented");
292 }
293
294 @Override
295 public String getResource() {
296 throw new AssertionError("Not implemented");
297 }
298
299 @Override
300 public int length() {
301 return value.length();
302 }
303
304 @Override
305 public char charAt(int index) {
306 return value.charAt(index);
307 }
308
309 @NonNull
310 @Override
311 public CharSequence subSequence(int start, int end) {
312 return value.subSequence(start, end);
313 }
314
315 @Override
316 public int compareTo(@NonNull Jid o) {
317 throw new AssertionError("Not implemented");
318 }
319
320 public static Jid getNullForInvalid(final Jid jid) {
321 if (jid instanceof Invalid) {
322 return null;
323 } else {
324 return jid;
325 }
326 }
327
328 public static boolean isValid(Jid jid) {
329 return !(jid instanceof Invalid);
330 }
331
332 public static boolean hasValidFrom(final Stanza stanza) {
333 final String from = stanza.getAttribute("from");
334 if (from == null) {
335 return false;
336 }
337 try {
338 Jid.of(from);
339 return true;
340 } catch (final IllegalArgumentException e) {
341 return false;
342 }
343 }
344 }
345}