1package eu.siacs.conversations.utils;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.util.Log;
6
7import androidx.annotation.NonNull;
8
9import com.google.common.base.Strings;
10import com.google.common.base.Throwables;
11import com.google.common.collect.ImmutableMap;
12import com.google.common.net.InetAddresses;
13import com.google.common.primitives.Ints;
14
15import java.io.IOException;
16import java.lang.reflect.Field;
17import java.net.Inet4Address;
18import java.net.InetAddress;
19import java.net.UnknownHostException;
20import java.util.Arrays;
21import java.util.ArrayList;
22import java.util.Collections;
23import java.util.List;
24import java.util.Map;
25
26//import de.gultsch.minidns.AndroidDNSClient;
27import org.minidns.AbstractDnsClient;
28import org.minidns.DnsCache;
29import org.minidns.DnsClient;
30import org.minidns.cache.LruCache;
31import org.minidns.dnsmessage.Question;
32import org.minidns.dnsname.DnsName;
33import org.minidns.dnssec.DnssecResultNotAuthenticException;
34import org.minidns.dnssec.DnssecValidationFailedException;
35import org.minidns.dnsserverlookup.AndroidUsingExec;
36import org.minidns.hla.DnssecResolverApi;
37import org.minidns.hla.ResolverApi;
38import org.minidns.hla.ResolverResult;
39import org.minidns.iterative.ReliableDnsClient;
40import org.minidns.record.A;
41import org.minidns.record.AAAA;
42import org.minidns.record.CNAME;
43import org.minidns.record.Data;
44import org.minidns.record.InternetAddressRR;
45import org.minidns.record.Record;
46import org.minidns.record.SRV;
47import eu.siacs.conversations.Config;
48import eu.siacs.conversations.R;
49import eu.siacs.conversations.services.XmppConnectionService;
50import eu.siacs.conversations.xmpp.Jid;
51
52public class Resolver {
53
54 public static final int DEFAULT_PORT_XMPP = 5222;
55
56 private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
57 private static final String STARTTLS_SERVICE = "_xmpp-client";
58
59 private static XmppConnectionService SERVICE = null;
60
61 private static List<String> DNSSECLESS_TLDS = Arrays.asList(
62 "ae",
63 "aero",
64 "ai",
65 "al",
66 "ao",
67 "aq",
68 "as",
69 "ba",
70 "bb",
71 "bd",
72 "bf",
73 "bi",
74 "bj",
75 "bn",
76 "bo",
77 "bs",
78 "bw",
79 "cd",
80 "cf",
81 "cg",
82 "ci",
83 "ck",
84 "cm",
85 "cu",
86 "cv",
87 "cw",
88 "dj",
89 "dm",
90 "do",
91 "ec",
92 "eg",
93 "eh",
94 "er",
95 "et",
96 "fj",
97 "fk",
98 "ga",
99 "ge",
100 "gf",
101 "gh",
102 "gm",
103 "gp",
104 "gq",
105 "gt",
106 "gu",
107 "hm",
108 "ht",
109 "im",
110 "ir",
111 "je",
112 "jm",
113 "jo",
114 "ke",
115 "kh",
116 "km",
117 "kn",
118 "kp",
119 "kz",
120 "ls",
121 "mg",
122 "mh",
123 "mk",
124 "ml",
125 "mm",
126 "mo",
127 "mp",
128 "mq",
129 "ms",
130 "mt",
131 "mu",
132 "mv",
133 "mw",
134 "mz",
135 "ne",
136 "ng",
137 "ni",
138 "np",
139 "nr",
140 "om",
141 "pa",
142 "pf",
143 "pg",
144 "pk",
145 "pn",
146 "ps",
147 "py",
148 "qa",
149 "rw",
150 "sd",
151 "sl",
152 "sm",
153 "so",
154 "sr",
155 "sv",
156 "sy",
157 "sz",
158 "tc",
159 "td",
160 "tg",
161 "tj",
162 "to",
163 "tr",
164 "va",
165 "vg",
166 "vi",
167 "ye",
168 "zm",
169 "zw"
170 );
171
172 protected static final Map<String, String> knownSRV = ImmutableMap.of(
173 "_xmpp-client._tcp.yax.im", "xmpp.yaxim.org",
174 "_xmpps-client._tcp.yax.im", "xmpp.yaxim.org",
175 "_xmpp-server._tcp.yax.im", "xmpp.yaxim.org"
176 );
177
178 public static void init(XmppConnectionService service) {
179 Resolver.SERVICE = service;
180 DnsClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
181 DnsClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
182 DnsClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
183 final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
184 if (client instanceof ReliableDnsClient) {
185 ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
186 }
187 }
188
189 public static List<Result> fromHardCoded(final String hostname, final int port) {
190 final Result result = new Result();
191 result.hostname = DnsName.from(hostname);
192 result.port = port;
193 result.directTls = useDirectTls(port);
194 result.authenticated = true;
195 return Collections.singletonList(result);
196 }
197
198 public static void checkDomain(final Jid jid) {
199 DnsName.from(jid.getDomain());
200 }
201
202 public static boolean invalidHostname(final String hostname) {
203 try {
204 DnsName.from(hostname);
205 return false;
206 } catch (IllegalArgumentException e) {
207 return true;
208 }
209 }
210
211 public static void clearCache() {
212 final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
213 final DnsCache dnsCache = client.getCache();
214 if (dnsCache instanceof LruCache) {
215 Log.d(Config.LOGTAG,"clearing DNS cache");
216 ((LruCache) dnsCache).clear();
217 }
218
219 final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
220 final DnsCache dnsCacheSec = clientSec.getCache();
221 if (dnsCacheSec instanceof LruCache) {
222 Log.d(Config.LOGTAG,"clearing DNSSEC cache");
223 ((LruCache) dnsCacheSec).clear();
224 }
225 }
226
227
228 public static boolean useDirectTls(final int port) {
229 return port == 443 || port == 5223;
230 }
231
232 public static List<Result> resolve(final String domain) {
233 final List<Result> ipResults = fromIpAddress(domain);
234 if (ipResults.size() > 0) {
235 return ipResults;
236 }
237 final List<Result> results = new ArrayList<>();
238 final List<Result> fallbackResults = new ArrayList<>();
239 final Thread[] threads = new Thread[3];
240 threads[0] = new Thread(() -> {
241 try {
242 final List<Result> list = resolveSrv(domain, true);
243 synchronized (results) {
244 results.addAll(list);
245 }
246 } catch (final Throwable throwable) {
247 if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
248 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
249 }
250 }
251 });
252 threads[1] = new Thread(() -> {
253 try {
254 final List<Result> list = resolveSrv(domain, false);
255 synchronized (results) {
256 results.addAll(list);
257 }
258 } catch (final Throwable throwable) {
259 if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
260 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
261 }
262 }
263 });
264 threads[2] = new Thread(() -> {
265 List<Result> list = resolveNoSrvRecords(DnsName.from(domain), true);
266 synchronized (fallbackResults) {
267 fallbackResults.addAll(list);
268 }
269 });
270 for (final Thread thread : threads) {
271 thread.start();
272 }
273 try {
274 threads[0].join();
275 threads[1].join();
276 if (results.size() > 0) {
277 threads[2].interrupt();
278 synchronized (results) {
279 Collections.sort(results);
280 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
281 return results;
282 }
283 } else {
284 threads[2].join();
285 synchronized (fallbackResults) {
286 Collections.sort(fallbackResults);
287 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
288 return fallbackResults;
289 }
290 }
291 } catch (InterruptedException e) {
292 for (Thread thread : threads) {
293 thread.interrupt();
294 }
295 return Collections.emptyList();
296 }
297 }
298
299 private static List<Result> fromIpAddress(String domain) {
300 if (!IP.matches(domain)) {
301 return Collections.emptyList();
302 }
303 try {
304 Result result = new Result();
305 result.ip = InetAddress.getByName(domain);
306 result.port = DEFAULT_PORT_XMPP;
307 result.authenticated = true;
308 return Collections.singletonList(result);
309 } catch (UnknownHostException e) {
310 return Collections.emptyList();
311 }
312 }
313
314 private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
315 final String dnsNameS = (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain;
316 DnsName dnsName = DnsName.from(dnsNameS);
317 ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
318 final List<Result> results = new ArrayList<>();
319 final List<Thread> threads = new ArrayList<>();
320 for (SRV record : result.getAnswersOrEmptySet()) {
321 if (record.name.length() == 0 && record.priority == 0) {
322 continue;
323 }
324 final boolean authentic = result.isAuthenticData() || record.target.toString().equals(knownSRV.get(dnsNameS));
325 threads.add(new Thread(() -> {
326 final List<Result> ipv4s = resolveIp(record, A.class, authentic, directTls);
327 if (ipv4s.size() == 0) {
328 Result resolverResult = Result.fromRecord(record, directTls);
329 resolverResult.authenticated = result.isAuthenticData();
330 ipv4s.add(resolverResult);
331 }
332 synchronized (results) {
333 results.addAll(ipv4s);
334 }
335
336 }));
337 threads.add(new Thread(() -> {
338 final List<Result> ipv6s = resolveIp(record, AAAA.class, authentic, directTls);
339 synchronized (results) {
340 results.addAll(ipv6s);
341 }
342 }));
343 }
344 for (Thread thread : threads) {
345 thread.start();
346 }
347 for (Thread thread : threads) {
348 try {
349 thread.join();
350 } catch (InterruptedException e) {
351 return Collections.emptyList();
352 }
353 }
354 return results;
355 }
356
357 private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
358 List<Result> list = new ArrayList<>();
359 try {
360 ResolverResult<D> results = resolveWithFallback(srv.target, type);
361 for (D record : results.getAnswersOrEmptySet()) {
362 Result resolverResult = Result.fromRecord(srv, directTls);
363 resolverResult.authenticated = results.isAuthenticData() && authenticated;
364 resolverResult.ip = record.getInetAddress();
365 list.add(resolverResult);
366 }
367 } catch (Throwable t) {
368 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
369 }
370 return list;
371 }
372
373 private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
374 final List<Result> results = new ArrayList<>();
375 try {
376 ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
377 for (A a : aResult.getAnswersOrEmptySet()) {
378 Result r = Result.createDefault(dnsName, a.getInetAddress());
379 r.authenticated = aResult.isAuthenticData();
380 results.add(r);
381 }
382 ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
383 for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
384 Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
385 r.authenticated = aaaaResult.isAuthenticData();
386 results.add(r);
387 }
388 if (results.size() == 0 && withCnames) {
389 ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
390 for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
391 for (Result r : resolveNoSrvRecords(cname.name, false)) {
392 r.authenticated = r.authenticated && cnameResult.isAuthenticData();
393 results.add(r);
394 }
395 }
396 }
397 } catch (final Throwable throwable) {
398 if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
399 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
400 }
401 }
402 results.add(Result.createDefault(dnsName));
403 return results;
404 }
405
406 private static <D extends Data> ResolverResult<D> resolveWithFallback(DnsName dnsName, Class<D> type) throws IOException {
407 final Question question = new Question(dnsName, Record.TYPE.getType(type));
408 if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
409 try {
410 ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
411 if (result.wasSuccessful() && !result.isAuthenticData()) {
412 Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
413 }
414 return result;
415 } catch (DnssecValidationFailedException e) {
416 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
417 } catch (IOException e) {
418 throw e;
419 } catch (Throwable throwable) {
420 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
421 }
422 }
423 return ResolverApi.INSTANCE.resolve(question);
424 }
425
426 public static class Result implements Comparable<Result> {
427 public static final String DOMAIN = "domain";
428 public static final String IP = "ip";
429 public static final String HOSTNAME = "hostname";
430 public static final String PORT = "port";
431 public static final String PRIORITY = "priority";
432 public static final String DIRECT_TLS = "directTls";
433 public static final String AUTHENTICATED = "authenticated";
434 private InetAddress ip;
435 private DnsName hostname;
436 private int port = DEFAULT_PORT_XMPP;
437 private boolean directTls = false;
438 private boolean authenticated = false;
439 private int priority;
440
441 static Result fromRecord(SRV srv, boolean directTls) {
442 Result result = new Result();
443 result.port = srv.port;
444 result.hostname = srv.name;
445 result.directTls = directTls;
446 result.priority = srv.priority;
447 return result;
448 }
449
450 static Result createDefault(DnsName hostname, InetAddress ip) {
451 Result result = new Result();
452 result.port = DEFAULT_PORT_XMPP;
453 result.hostname = hostname;
454 result.ip = ip;
455 return result;
456 }
457
458 static Result createDefault(DnsName hostname) {
459 return createDefault(hostname, null);
460 }
461
462 public static Result fromCursor(Cursor cursor) {
463 final Result result = new Result();
464 try {
465 result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
466 } catch (UnknownHostException e) {
467 result.ip = null;
468 }
469 final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
470 result.hostname = hostname == null ? null : DnsName.from(hostname);
471 result.port = cursor.getInt(cursor.getColumnIndex(PORT));
472 result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
473 result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
474 result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
475 return result;
476 }
477
478 @Override
479 public boolean equals(Object o) {
480 if (this == o) return true;
481 if (o == null || getClass() != o.getClass()) return false;
482
483 Result result = (Result) o;
484
485 if (port != result.port) return false;
486 if (directTls != result.directTls) return false;
487 if (authenticated != result.authenticated) return false;
488 if (priority != result.priority) return false;
489 if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
490 return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
491 }
492
493 @Override
494 public int hashCode() {
495 int result = ip != null ? ip.hashCode() : 0;
496 result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
497 result = 31 * result + port;
498 result = 31 * result + (directTls ? 1 : 0);
499 result = 31 * result + (authenticated ? 1 : 0);
500 result = 31 * result + priority;
501 return result;
502 }
503
504 public InetAddress getIp() {
505 return ip;
506 }
507
508 public int getPort() {
509 return port;
510 }
511
512 public DnsName getHostname() {
513 return hostname;
514 }
515
516 public boolean isDirectTls() {
517 return directTls;
518 }
519
520 public boolean isAuthenticated() {
521 return authenticated;
522 }
523
524 @Override
525 public String toString() {
526 return "Result{" +
527 "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
528 ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' +
529 ", port=" + port +
530 ", directTls=" + directTls +
531 ", authenticated=" + authenticated +
532 ", priority=" + priority +
533 '}';
534 }
535
536 @Override
537 public int compareTo(@NonNull Result result) {
538 if (result.priority == priority) {
539 if (directTls == result.directTls) {
540 if (ip == null && result.ip == null) {
541 return 0;
542 } else if (ip != null && result.ip != null) {
543 if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
544 return 0;
545 } else {
546 return ip instanceof Inet4Address ? -1 : 1;
547 }
548 } else {
549 return ip != null ? -1 : 1;
550 }
551 } else {
552 return directTls ? 1 : -1;
553 }
554 } else {
555 return priority - result.priority;
556 }
557 }
558
559 public ContentValues toContentValues() {
560 final ContentValues contentValues = new ContentValues();
561 contentValues.put(IP, ip == null ? null : ip.getAddress());
562 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
563 contentValues.put(PORT, port);
564 contentValues.put(PRIORITY, priority);
565 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
566 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
567 return contentValues;
568 }
569
570 public Result seeOtherHost(final String seeOtherHost) {
571 final String hostname = seeOtherHost.trim();
572 if (hostname.isEmpty()) {
573 return null;
574 }
575 final Result result = new Result();
576 result.directTls = this.directTls;
577 final int portSegmentStart = hostname.lastIndexOf(':');
578 if (hostname.charAt(hostname.length() - 1) != ']'
579 && portSegmentStart >= 0
580 && hostname.length() >= portSegmentStart + 1) {
581 final String hostPart = hostname.substring(0, portSegmentStart);
582 final String portPart = hostname.substring(portSegmentStart + 1);
583 final Integer port = Ints.tryParse(portPart);
584 if (port == null || Strings.isNullOrEmpty(hostPart)) {
585 return null;
586 }
587 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
588 result.port = port;
589 if (InetAddresses.isInetAddress(host)) {
590 final InetAddress inetAddress;
591 try {
592 inetAddress = InetAddresses.forString(host);
593 } catch (final IllegalArgumentException e) {
594 return null;
595 }
596 result.ip = inetAddress;
597 } else {
598 if (hostPart.trim().isEmpty()) {
599 return null;
600 }
601 try {
602 result.hostname = DnsName.from(hostPart.trim());
603 } catch (final Exception e) {
604 return null;
605 }
606 }
607 } else {
608 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
609 if (InetAddresses.isInetAddress(host)) {
610 final InetAddress inetAddress;
611 try {
612 inetAddress = InetAddresses.forString(host);
613 } catch (final IllegalArgumentException e) {
614 return null;
615 }
616 result.ip = inetAddress;
617 } else {
618 try {
619 result.hostname = DnsName.from(hostname);
620 } catch (final Exception e) {
621 return null;
622 }
623 }
624 result.port = port;
625 }
626 return result;
627 }
628 }
629
630}