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