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