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 final AbstractDnsClient dnssecclient = DnssecResolverApi.INSTANCE.getClient();
242 if (client instanceof ReliableDnsClient) {
243 ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
244 }
245 }
246
247 public static List<Result> fromHardCoded(final String hostname, final int port) {
248 final Result result = new Result();
249 result.hostname = DnsName.from(hostname);
250 result.port = port;
251 result.directTls = useDirectTls(port);
252 result.authenticated = true;
253 return Collections.singletonList(result);
254 }
255
256 public static void checkDomain(final Jid jid) {
257 DnsName.from(jid.getDomain());
258 }
259
260 public static boolean invalidHostname(final String hostname) {
261 try {
262 DnsName.from(hostname);
263 return false;
264 } catch (final InvalidDnsNameException | IllegalArgumentException e) {
265 return true;
266 }
267 }
268
269 public static void clearCache() {
270 final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
271 final DnsCache dnsCache = client.getCache();
272 if (dnsCache instanceof LruCache) {
273 Log.d(Config.LOGTAG,"clearing DNS cache");
274 ((LruCache) dnsCache).clear();
275 }
276
277 final AbstractDnsClient clientSec = DnssecResolverApi.INSTANCE.getClient();
278 final DnsCache dnsCacheSec = clientSec.getCache();
279 if (dnsCacheSec instanceof LruCache) {
280 Log.d(Config.LOGTAG,"clearing DNSSEC cache");
281 ((LruCache) dnsCacheSec).clear();
282 }
283 }
284
285 public static boolean useDirectTls(final int port) {
286 return port == 443 || port == 5223;
287 }
288
289 public static List<Result> resolve(final String domain) {
290 final List<Result> ipResults = fromIpAddress(domain);
291 if (!ipResults.isEmpty()) {
292 return ipResults;
293 }
294
295 final var startTls = resolveSrvAsFuture(domain, false);
296 final var directTls = resolveSrvAsFuture(domain, true);
297
298 final var combined = merge(ImmutableList.of(startTls, directTls));
299
300 final var combinedWithFallback =
301 Futures.transformAsync(
302 combined,
303 results -> {
304 if (results.isEmpty()) {
305 return resolveNoSrvAsFuture(DnsName.from(domain), true);
306 } else {
307 return Futures.immediateFuture(results);
308 }
309 },
310 MoreExecutors.directExecutor());
311 final var orderedFuture =
312 Futures.transform(
313 combinedWithFallback,
314 all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
315 MoreExecutors.directExecutor());
316 try {
317 final var ordered = orderedFuture.get();
318 Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
319 return ordered;
320 } catch (final ExecutionException e) {
321 Log.d(Config.LOGTAG, "error resolving DNS", e);
322 return Collections.emptyList();
323 } catch (final InterruptedException e) {
324 Log.d(Config.LOGTAG, "DNS resolution interrupted");
325 return Collections.emptyList();
326 }
327 }
328
329 private static List<Result> fromIpAddress(final String domain) {
330 if (IP.matches(domain)) {
331 final InetAddress inetAddress;
332 try {
333 inetAddress = InetAddress.getByName(domain);
334 } catch (final UnknownHostException e) {
335 return Collections.emptyList();
336 }
337 final Result result = new Result();
338 result.ip = inetAddress;
339 result.port = DEFAULT_PORT_XMPP;
340 return Collections.singletonList(result);
341 } else {
342 return Collections.emptyList();
343 }
344 }
345
346 private static ListenableFuture<List<Result>> resolveSrvAsFuture(
347 final String domain, final boolean directTls) {
348 final DnsName dnsName =
349 DnsName.from(
350 (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
351 final var resultFuture = resolveAsFuture(dnsName, SRV.class);
352 return Futures.transformAsync(
353 resultFuture,
354 result -> resolveIpsAsFuture(result, directTls),
355 MoreExecutors.directExecutor());
356 }
357
358 @NonNull
359 private static ListenableFuture<List<Result>> resolveIpsAsFuture(
360 final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
361 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
362 new ImmutableList.Builder<>();
363 for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
364 if (record.target.length() == 0 && record.priority == 0) {
365 continue;
366 }
367 final var ipv4sRaw =
368 resolveIpsAsFuture(
369 record, A.class, srvResolverResult.isAuthenticData(), directTls);
370 final var ipv4s =
371 Futures.transform(
372 ipv4sRaw,
373 results -> {
374 if (results.isEmpty()) {
375 final Result resolverResult =
376 Result.fromRecord(record, directTls);
377 resolverResult.authenticated =
378 srvResolverResult.isAuthenticData();
379 return Collections.singletonList(resolverResult);
380 } else {
381 return results;
382 }
383 },
384 MoreExecutors.directExecutor());
385 final var ipv6s =
386 resolveIpsAsFuture(
387 record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
388 futuresBuilder.add(ipv4s);
389 futuresBuilder.add(ipv6s);
390 }
391 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
392 return merge(futures);
393 }
394
395 private static ListenableFuture<List<Result>> merge(
396 final Collection<ListenableFuture<List<Result>>> futures) {
397 return Futures.transform(
398 Futures.successfulAsList(futures),
399 lists -> {
400 final var builder = new ImmutableList.Builder<Result>();
401 for (final Collection<Result> list : lists) {
402 if (list == null) {
403 continue;
404 }
405 builder.addAll(list);
406 }
407 return builder.build();
408 },
409 MoreExecutors.directExecutor());
410 }
411
412 private static <D extends InternetAddressRR<?>>
413 ListenableFuture<List<Result>> resolveIpsAsFuture(
414 final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
415 final var resultFuture = resolveAsFuture(srv.target, type);
416 return Futures.transform(
417 resultFuture,
418 result -> {
419 final var builder = new ImmutableList.Builder<Result>();
420 for (D record : result.getAnswersOrEmptySet()) {
421 Result resolverResult = Result.fromRecord(srv, directTls);
422 resolverResult.authenticated =
423 result.isAuthenticData()
424 && authenticated; // TODO technically it does not matter if
425 // the IP
426 // was authenticated
427 resolverResult.ip = record.getInetAddress();
428 builder.add(resolverResult);
429 }
430 return builder.build();
431 },
432 MoreExecutors.directExecutor());
433 }
434
435 private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
436 final DnsName dnsName, boolean cName) {
437 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
438 new ImmutableList.Builder<>();
439 ListenableFuture<List<Result>> aRecordResults =
440 Futures.transform(
441 resolveAsFuture(dnsName, A.class),
442 result ->
443 Lists.transform(
444 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
445 a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())),
446 MoreExecutors.directExecutor());
447 futuresBuilder.add(aRecordResults);
448 ListenableFuture<List<Result>> aaaaRecordResults =
449 Futures.transform(
450 resolveAsFuture(dnsName, AAAA.class),
451 result ->
452 Lists.transform(
453 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
454 aaaa ->
455 Result.createDefault(
456 dnsName, aaaa.getInetAddress(), result.isAuthenticData())),
457 MoreExecutors.directExecutor());
458 futuresBuilder.add(aaaaRecordResults);
459 if (cName) {
460 ListenableFuture<List<Result>> cNameRecordResults =
461 Futures.transformAsync(
462 resolveAsFuture(dnsName, CNAME.class),
463 result -> {
464 Collection<ListenableFuture<List<Result>>> test =
465 Lists.transform(
466 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
467 cname -> resolveNoSrvAsFuture(cname.target, false));
468 return merge(test);
469 },
470 MoreExecutors.directExecutor());
471 futuresBuilder.add(cNameRecordResults);
472 }
473 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
474 final var noSrvFallbacks = merge(futures);
475 return Futures.transform(
476 noSrvFallbacks,
477 results -> {
478 if (results.isEmpty()) {
479 return Collections.singletonList(Result.createDefault(dnsName));
480 } else {
481 return results;
482 }
483 },
484 MoreExecutors.directExecutor());
485 }
486
487 private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
488 final DnsName dnsName, final Class<D> type) {
489 return Futures.submit(
490 () -> {
491 final Question question = new Question(dnsName, Record.TYPE.getType(type));
492 if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
493 try {
494 ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
495 if (result.wasSuccessful() && !result.isAuthenticData()) {
496 Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
497 }
498 return result;
499 } catch (DnssecValidationFailedException e) {
500 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
501 } catch (Throwable throwable) {
502 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
503 }
504 }
505 return ResolverApi.INSTANCE.resolve(question);
506 },
507 DNS_QUERY_EXECUTOR);
508 }
509
510 public static class Result {
511 public static final String DOMAIN = "domain";
512 public static final String IP = "ip";
513 public static final String HOSTNAME = "hostname";
514 public static final String PORT = "port";
515 public static final String PRIORITY = "priority";
516 public static final String DIRECT_TLS = "directTls";
517 public static final String AUTHENTICATED = "authenticated";
518 private InetAddress ip;
519 private DnsName hostname;
520 private int port = DEFAULT_PORT_XMPP;
521 private boolean directTls = false;
522 private boolean authenticated = false;
523 private int priority;
524
525 static Result fromRecord(final SRV srv, final boolean directTls) {
526 final Result result = new Result();
527 result.port = srv.port;
528 result.hostname = srv.target;
529 result.directTls = directTls;
530 result.priority = srv.priority;
531 return result;
532 }
533
534 static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
535 Result result = new Result();
536 result.port = DEFAULT_PORT_XMPP;
537 result.hostname = hostname;
538 result.ip = ip;
539 result.authenticated = authenticated;
540 return result;
541 }
542
543 static Result createDefault(final DnsName hostname) {
544 return createDefault(hostname, null, false);
545 }
546
547 public static Result fromCursor(final Cursor cursor) {
548 final Result result = new Result();
549 try {
550 result.ip =
551 InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
552 } catch (final UnknownHostException e) {
553 result.ip = null;
554 }
555 final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
556 result.hostname = hostname == null ? null : DnsName.from(hostname);
557 result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
558 result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
559 result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
560 result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
561 return result;
562 }
563
564 @Override
565 public boolean equals(Object o) {
566 if (this == o) return true;
567 if (o == null || getClass() != o.getClass()) return false;
568 Result result = (Result) o;
569 return port == result.port
570 && directTls == result.directTls
571 && authenticated == result.authenticated
572 && priority == result.priority
573 && Objects.equal(ip, result.ip)
574 && Objects.equal(hostname, result.hostname);
575 }
576
577 @Override
578 public int hashCode() {
579 return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
580 }
581
582 public InetAddress getIp() {
583 return ip;
584 }
585
586 public int getPort() {
587 return port;
588 }
589
590 public DnsName getHostname() {
591 return hostname;
592 }
593
594 public boolean isDirectTls() {
595 return directTls;
596 }
597
598 public boolean isAuthenticated() {
599 return authenticated;
600 }
601
602 @Override
603 @NonNull
604 public String toString() {
605 return MoreObjects.toStringHelper(this)
606 .add("ip", ip)
607 .add("hostname", hostname)
608 .add("port", port)
609 .add("directTls", directTls)
610 .add("authenticated", authenticated)
611 .add("priority", priority)
612 .toString();
613 }
614
615 public ContentValues toContentValues() {
616 final ContentValues contentValues = new ContentValues();
617 contentValues.put(IP, ip == null ? null : ip.getAddress());
618 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
619 contentValues.put(PORT, port);
620 contentValues.put(PRIORITY, priority);
621 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
622 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
623 return contentValues;
624 }
625
626 public Result seeOtherHost(final String seeOtherHost) {
627 final String hostname = seeOtherHost.trim();
628 if (hostname.isEmpty()) {
629 return null;
630 }
631 final Result result = new Result();
632 result.directTls = this.directTls;
633 final int portSegmentStart = hostname.lastIndexOf(':');
634 if (hostname.charAt(hostname.length() - 1) != ']'
635 && portSegmentStart >= 0
636 && hostname.length() >= portSegmentStart + 1) {
637 final String hostPart = hostname.substring(0, portSegmentStart);
638 final String portPart = hostname.substring(portSegmentStart + 1);
639 final Integer port = Ints.tryParse(portPart);
640 if (port == null || Strings.isNullOrEmpty(hostPart)) {
641 return null;
642 }
643 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
644 result.port = port;
645 if (InetAddresses.isInetAddress(host)) {
646 final InetAddress inetAddress;
647 try {
648 inetAddress = InetAddresses.forString(host);
649 } catch (final IllegalArgumentException e) {
650 return null;
651 }
652 result.ip = inetAddress;
653 } else {
654 if (hostPart.trim().isEmpty()) {
655 return null;
656 }
657 try {
658 result.hostname = DnsName.from(hostPart.trim());
659 } catch (final Exception e) {
660 return null;
661 }
662 }
663 } else {
664 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
665 if (InetAddresses.isInetAddress(host)) {
666 final InetAddress inetAddress;
667 try {
668 inetAddress = InetAddresses.forString(host);
669 } catch (final IllegalArgumentException e) {
670 return null;
671 }
672 result.ip = inetAddress;
673 } else {
674 try {
675 result.hostname = DnsName.from(hostname);
676 } catch (final Exception e) {
677 return null;
678 }
679 }
680 result.port = port;
681 }
682 return result;
683 }
684 }
685}