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 (Throwable throwable) {
498 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
499 }
500 }
501 return ResolverApi.INSTANCE.resolve(question);
502 },
503 DNS_QUERY_EXECUTOR);
504 }
505
506 public static class Result {
507 public static final String DOMAIN = "domain";
508 public static final String IP = "ip";
509 public static final String HOSTNAME = "hostname";
510 public static final String PORT = "port";
511 public static final String PRIORITY = "priority";
512 public static final String DIRECT_TLS = "directTls";
513 public static final String AUTHENTICATED = "authenticated";
514 private InetAddress ip;
515 private DnsName hostname;
516 private int port = DEFAULT_PORT_XMPP;
517 private boolean directTls = false;
518 private boolean authenticated = false;
519 private int priority;
520
521 static Result fromRecord(final SRV srv, final boolean directTls) {
522 final Result result = new Result();
523 result.port = srv.port;
524 result.hostname = srv.target;
525 result.directTls = directTls;
526 result.priority = srv.priority;
527 return result;
528 }
529
530 static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
531 Result result = new Result();
532 result.port = DEFAULT_PORT_XMPP;
533 result.hostname = hostname;
534 result.ip = ip;
535 result.authenticated = authenticated;
536 return result;
537 }
538
539 static Result createDefault(final DnsName hostname) {
540 return createDefault(hostname, null, false);
541 }
542
543 public static Result fromCursor(final Cursor cursor) {
544 final Result result = new Result();
545 try {
546 result.ip =
547 InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
548 } catch (final UnknownHostException e) {
549 result.ip = null;
550 }
551 final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
552 result.hostname = hostname == null ? null : DnsName.from(hostname);
553 result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
554 result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
555 result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
556 result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
557 return result;
558 }
559
560 @Override
561 public boolean equals(Object o) {
562 if (this == o) return true;
563 if (o == null || getClass() != o.getClass()) return false;
564 Result result = (Result) o;
565 return port == result.port
566 && directTls == result.directTls
567 && authenticated == result.authenticated
568 && priority == result.priority
569 && Objects.equal(ip, result.ip)
570 && Objects.equal(hostname, result.hostname);
571 }
572
573 @Override
574 public int hashCode() {
575 return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
576 }
577
578 public InetAddress getIp() {
579 return ip;
580 }
581
582 public int getPort() {
583 return port;
584 }
585
586 public DnsName getHostname() {
587 return hostname;
588 }
589
590 public boolean isDirectTls() {
591 return directTls;
592 }
593
594 public boolean isAuthenticated() {
595 return authenticated;
596 }
597
598 @Override
599 @NonNull
600 public String toString() {
601 return MoreObjects.toStringHelper(this)
602 .add("ip", ip)
603 .add("hostname", hostname)
604 .add("port", port)
605 .add("directTls", directTls)
606 .add("authenticated", authenticated)
607 .add("priority", priority)
608 .toString();
609 }
610
611 public ContentValues toContentValues() {
612 final ContentValues contentValues = new ContentValues();
613 contentValues.put(IP, ip == null ? null : ip.getAddress());
614 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
615 contentValues.put(PORT, port);
616 contentValues.put(PRIORITY, priority);
617 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
618 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
619 return contentValues;
620 }
621
622 public Result seeOtherHost(final String seeOtherHost) {
623 final String hostname = seeOtherHost.trim();
624 if (hostname.isEmpty()) {
625 return null;
626 }
627 final Result result = new Result();
628 result.directTls = this.directTls;
629 final int portSegmentStart = hostname.lastIndexOf(':');
630 if (hostname.charAt(hostname.length() - 1) != ']'
631 && portSegmentStart >= 0
632 && hostname.length() >= portSegmentStart + 1) {
633 final String hostPart = hostname.substring(0, portSegmentStart);
634 final String portPart = hostname.substring(portSegmentStart + 1);
635 final Integer port = Ints.tryParse(portPart);
636 if (port == null || Strings.isNullOrEmpty(hostPart)) {
637 return null;
638 }
639 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
640 result.port = port;
641 if (InetAddresses.isInetAddress(host)) {
642 final InetAddress inetAddress;
643 try {
644 inetAddress = InetAddresses.forString(host);
645 } catch (final IllegalArgumentException e) {
646 return null;
647 }
648 result.ip = inetAddress;
649 } else {
650 if (hostPart.trim().isEmpty()) {
651 return null;
652 }
653 try {
654 result.hostname = DnsName.from(hostPart.trim());
655 } catch (final Exception e) {
656 return null;
657 }
658 }
659 } else {
660 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
661 if (InetAddresses.isInetAddress(host)) {
662 final InetAddress inetAddress;
663 try {
664 inetAddress = InetAddresses.forString(host);
665 } catch (final IllegalArgumentException e) {
666 return null;
667 }
668 result.ip = inetAddress;
669 } else {
670 try {
671 result.hostname = DnsName.from(hostname);
672 } catch (final Exception e) {
673 return null;
674 }
675 }
676 result.port = port;
677 }
678 return result;
679 }
680 }
681}