1package eu.siacs.conversations.utils;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.util.Log;
6import androidx.annotation.NonNull;
7import com.google.common.base.MoreObjects;
8import com.google.common.base.Objects;
9import com.google.common.base.Strings;
10import com.google.common.collect.Collections2;
11import com.google.common.collect.ImmutableList;
12import com.google.common.collect.Lists;
13import com.google.common.collect.Ordering;
14import com.google.common.net.InetAddresses;
15import com.google.common.primitives.Ints;
16import com.google.common.util.concurrent.Futures;
17import com.google.common.util.concurrent.ListenableFuture;
18import com.google.common.util.concurrent.MoreExecutors;
19import de.gultsch.common.FutureMerger;
20import de.gultsch.minidns.AndroidDNSClient;
21import de.gultsch.minidns.ResolverResult;
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.Conversations;
24import eu.siacs.conversations.xmpp.Jid;
25import java.net.Inet4Address;
26import java.net.InetAddress;
27import java.net.UnknownHostException;
28import java.util.Arrays;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.Comparator;
32import java.util.List;
33import java.util.concurrent.ExecutionException;
34import java.util.concurrent.ExecutorService;
35import java.util.concurrent.Executors;
36import org.minidns.dnsmessage.Question;
37import org.minidns.dnsname.DnsName;
38import org.minidns.dnsname.InvalidDnsNameException;
39import org.minidns.dnsqueryresult.DnsQueryResult;
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;
47
48public class Resolver {
49
50 private static final Comparator<Result> RESULT_COMPARATOR =
51 (left, right) -> {
52 if (left.priority == right.priority) {
53 if (left.directTls == right.directTls) {
54 if (left.ip == null && right.ip == null) {
55 return 0;
56 } else if (left.ip != null && right.ip != null) {
57 if (left.ip instanceof Inet4Address
58 && right.ip instanceof Inet4Address) {
59 return 0;
60 } else {
61 return left.ip instanceof Inet4Address ? -1 : 1;
62 }
63 } else {
64 return left.ip != null ? -1 : 1;
65 }
66 } else {
67 return left.directTls ? -1 : 1;
68 }
69 } else {
70 return left.priority - right.priority;
71 }
72 };
73
74 private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12);
75
76 public static final int XMPP_PORT_STARTTLS = 5222;
77 private static final int XMPP_PORT_DIRECT_TLS = 5223;
78
79 private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
80 private static final String STARTTLS_SERVICE = "_xmpp-client";
81
82 public static List<Result> fromHardCoded(final String hostname, final int port) {
83 final Result result = new Result();
84 result.hostname = DnsName.from(hostname);
85 result.port = port;
86 result.directTls = useDirectTls(port);
87 result.authenticated = true;
88 return Collections.singletonList(result);
89 }
90
91 public static void checkDomain(final Jid jid) {
92 DnsName.from(jid.getDomain());
93 }
94
95 public static boolean invalidHostname(final String hostname) {
96 try {
97 DnsName.from(hostname);
98 return false;
99 } catch (final InvalidDnsNameException | IllegalArgumentException e) {
100 return true;
101 }
102 }
103
104 public static void clearCache() {}
105
106 public static boolean useDirectTls(final int port) {
107 return port == 443 || port == XMPP_PORT_DIRECT_TLS;
108 }
109
110 public static List<Result> resolve(final String domain) {
111 final List<Result> ipResults = fromIpAddress(domain);
112 if (!ipResults.isEmpty()) {
113 return ipResults;
114 }
115
116 final var startTls = resolveSrvAsFuture(domain, false);
117 final var directTls = resolveSrvAsFuture(domain, true);
118
119 final var combined = FutureMerger.successfulAsList(ImmutableList.of(startTls, directTls));
120
121 final var combinedWithFallback =
122 Futures.transformAsync(
123 combined,
124 results -> {
125 if (results.isEmpty()) {
126 return resolveNoSrvAsFuture(DnsName.from(domain), true);
127 } else {
128 return Futures.immediateFuture(results);
129 }
130 },
131 MoreExecutors.directExecutor());
132 final var orderedFuture =
133 Futures.transform(
134 combinedWithFallback,
135 all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
136 MoreExecutors.directExecutor());
137 try {
138 final var ordered = orderedFuture.get();
139 Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
140 return ordered;
141 } catch (final ExecutionException e) {
142 Log.d(Config.LOGTAG, "error resolving DNS", e);
143 return Collections.emptyList();
144 } catch (final InterruptedException e) {
145 Log.d(Config.LOGTAG, "DNS resolution interrupted");
146 return Collections.emptyList();
147 }
148 }
149
150 private static List<Result> fromIpAddress(final String domain) {
151 if (IP.matches(domain)) {
152 final InetAddress inetAddress;
153 try {
154 inetAddress = InetAddresses.forString(domain);
155 } catch (final IllegalArgumentException e) {
156 return Collections.emptyList();
157 }
158 return Result.createWithDefaultPorts(null, inetAddress);
159 } else {
160 return Collections.emptyList();
161 }
162 }
163
164 private static ListenableFuture<List<Result>> resolveSrvAsFuture(
165 final String domain, final boolean directTls) {
166 final DnsName dnsName =
167 DnsName.from(
168 (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
169 final var resultFuture = resolveAsFuture(dnsName, SRV.class);
170 return Futures.transformAsync(
171 resultFuture,
172 result -> resolveIpsAsFuture(result, directTls),
173 MoreExecutors.directExecutor());
174 }
175
176 @NonNull
177 private static ListenableFuture<List<Result>> resolveIpsAsFuture(
178 final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
179 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
180 new ImmutableList.Builder<>();
181 for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
182 if (record.target.length() == 0 && record.priority == 0) {
183 continue;
184 }
185 final var ipv4sRaw =
186 resolveIpsAsFuture(
187 record, A.class, srvResolverResult.isAuthenticData(), directTls);
188 final var ipv4s =
189 Futures.transform(
190 ipv4sRaw,
191 results -> {
192 if (results.isEmpty()) {
193 final Result resolverResult =
194 Result.fromRecord(record, directTls);
195 resolverResult.authenticated =
196 srvResolverResult.isAuthenticData();
197 return Collections.singletonList(resolverResult);
198 } else {
199 return results;
200 }
201 },
202 MoreExecutors.directExecutor());
203 final var ipv6s =
204 resolveIpsAsFuture(
205 record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
206 futuresBuilder.add(ipv4s);
207 futuresBuilder.add(ipv6s);
208 }
209 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
210 return FutureMerger.successfulAsList(futures);
211 }
212
213 private static ListenableFuture<List<Result>> merge(
214 final Collection<ListenableFuture<List<Result>>> futures) {
215 return Futures.transform(
216 Futures.successfulAsList(futures),
217 lists -> {
218 final var builder = new ImmutableList.Builder<Result>();
219 for (final Collection<Result> list : lists) {
220 if (list == null) {
221 continue;
222 }
223 builder.addAll(list);
224 }
225 return builder.build();
226 },
227 MoreExecutors.directExecutor());
228 }
229
230 private static <D extends InternetAddressRR<?>>
231 ListenableFuture<List<Result>> resolveIpsAsFuture(
232 final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
233 final var resultFuture = resolveAsFuture(srv.target, type);
234 return Futures.transform(
235 resultFuture,
236 result -> {
237 final var builder = new ImmutableList.Builder<Result>();
238 for (D record : result.getAnswersOrEmptySet()) {
239 Result resolverResult = Result.fromRecord(srv, directTls);
240 resolverResult.authenticated =
241 result.isAuthenticData()
242 && authenticated; // TODO technically it does not matter if
243 // the IP
244 // was authenticated
245 resolverResult.ip = record.getInetAddress();
246 builder.add(resolverResult);
247 }
248 return builder.build();
249 },
250 MoreExecutors.directExecutor());
251 }
252
253 private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
254 final DnsName dnsName, boolean cName) {
255 final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
256 new ImmutableList.Builder<>();
257 ListenableFuture<List<Result>> aRecordResults =
258 Futures.transform(
259 resolveAsFuture(dnsName, A.class),
260 result ->
261 Result.createDefaults(
262 dnsName,
263 Collections2.transform(
264 result.getAnswersOrEmptySet(),
265 InternetAddressRR::getInetAddress)),
266 MoreExecutors.directExecutor());
267 futuresBuilder.add(aRecordResults);
268 ListenableFuture<List<Result>> aaaaRecordResults =
269 Futures.transform(
270 resolveAsFuture(dnsName, AAAA.class),
271 result ->
272 Result.createDefaults(
273 dnsName,
274 Collections2.transform(
275 result.getAnswersOrEmptySet(),
276 InternetAddressRR::getInetAddress)),
277 MoreExecutors.directExecutor());
278 futuresBuilder.add(aaaaRecordResults);
279 if (cName) {
280 ListenableFuture<List<Result>> cNameRecordResults =
281 Futures.transformAsync(
282 resolveAsFuture(dnsName, CNAME.class),
283 result -> {
284 Collection<ListenableFuture<List<Result>>> test =
285 Lists.transform(
286 ImmutableList.copyOf(result.getAnswersOrEmptySet()),
287 cname -> resolveNoSrvAsFuture(cname.target, false));
288 return FutureMerger.successfulAsList(test);
289 },
290 MoreExecutors.directExecutor());
291 futuresBuilder.add(cNameRecordResults);
292 }
293 final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
294 final var noSrvFallbacks = FutureMerger.successfulAsList(futures);
295 return Futures.transform(
296 noSrvFallbacks,
297 results -> {
298 if (results.isEmpty()) {
299 return Result.createDefaults(dnsName);
300 } else {
301 return results;
302 }
303 },
304 MoreExecutors.directExecutor());
305 }
306
307 private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
308 final DnsName dnsName, final Class<D> type) {
309 return Futures.submit(
310 () -> {
311 final Question question = new Question(dnsName, Record.TYPE.getType(type));
312 final AndroidDNSClient androidDNSClient =
313 new AndroidDNSClient(Conversations.getContext());
314 final DnsQueryResult dnsQueryResult = androidDNSClient.query(question);
315 return new ResolverResult<>(question, dnsQueryResult, null);
316 },
317 DNS_QUERY_EXECUTOR);
318 }
319
320 public static class Result {
321 public static final String DOMAIN = "domain";
322 public static final String IP = "ip";
323 public static final String HOSTNAME = "hostname";
324 public static final String PORT = "port";
325 public static final String PRIORITY = "priority";
326 public static final String DIRECT_TLS = "directTls";
327 public static final String AUTHENTICATED = "authenticated";
328 private InetAddress ip;
329 private DnsName hostname;
330 private int port = XMPP_PORT_STARTTLS;
331 private boolean directTls = false;
332 private boolean authenticated = false;
333 private int priority;
334
335 static Result fromRecord(final SRV srv, final boolean directTls) {
336 final Result result = new Result();
337 result.port = srv.port;
338 result.hostname = srv.target;
339 result.directTls = directTls;
340 result.priority = srv.priority;
341 return result;
342 }
343
344 static List<Result> createWithDefaultPorts(final DnsName hostname, final InetAddress ip) {
345 return Lists.transform(
346 Arrays.asList(XMPP_PORT_STARTTLS, XMPP_PORT_DIRECT_TLS),
347 p -> createDefault(hostname, ip, p));
348 }
349
350 static Result createDefault(final DnsName hostname, final InetAddress ip, final int port) {
351 Result result = new Result();
352 result.port = port;
353 result.hostname = hostname;
354 result.ip = ip;
355 result.directTls = useDirectTls(port);
356 return result;
357 }
358
359 static List<Result> createDefaults(
360 final DnsName hostname, final Collection<InetAddress> inetAddresses) {
361 final ImmutableList.Builder<Result> builder = new ImmutableList.Builder<>();
362 for (final InetAddress inetAddress : inetAddresses) {
363 builder.addAll(createWithDefaultPorts(hostname, inetAddress));
364 }
365 return builder.build();
366 }
367
368 static List<Result> createDefaults(final DnsName hostname) {
369 return createWithDefaultPorts(hostname, null);
370 }
371
372 public static Result fromCursor(final Cursor cursor) {
373 final Result result = new Result();
374 try {
375 result.ip =
376 InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
377 } catch (final UnknownHostException e) {
378 result.ip = null;
379 }
380 final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
381 result.hostname = hostname == null ? null : DnsName.from(hostname);
382 result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
383 result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
384 result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
385 result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
386 return result;
387 }
388
389 @Override
390 public boolean equals(Object o) {
391 if (this == o) return true;
392 if (o == null || getClass() != o.getClass()) return false;
393 Result result = (Result) o;
394 return port == result.port
395 && directTls == result.directTls
396 && authenticated == result.authenticated
397 && priority == result.priority
398 && Objects.equal(ip, result.ip)
399 && Objects.equal(hostname, result.hostname);
400 }
401
402 @Override
403 public int hashCode() {
404 return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
405 }
406
407 public InetAddress getIp() {
408 return ip;
409 }
410
411 public int getPort() {
412 return port;
413 }
414
415 public DnsName getHostname() {
416 return hostname;
417 }
418
419 public boolean isDirectTls() {
420 return directTls;
421 }
422
423 public boolean isAuthenticated() {
424 return authenticated;
425 }
426
427 @Override
428 @NonNull
429 public String toString() {
430 return MoreObjects.toStringHelper(this)
431 .add("ip", ip)
432 .add("hostname", hostname)
433 .add("port", port)
434 .add("directTls", directTls)
435 .add("authenticated", authenticated)
436 .add("priority", priority)
437 .toString();
438 }
439
440 public String asDestination() {
441 return ip != null ? InetAddresses.toAddrString(ip) : hostname.toString();
442 }
443
444 public ContentValues toContentValues() {
445 final ContentValues contentValues = new ContentValues();
446 contentValues.put(IP, ip == null ? null : ip.getAddress());
447 contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
448 contentValues.put(PORT, port);
449 contentValues.put(PRIORITY, priority);
450 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
451 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
452 return contentValues;
453 }
454
455 public Result seeOtherHost(final String seeOtherHost) {
456 final String hostname = seeOtherHost.trim();
457 if (hostname.isEmpty()) {
458 return null;
459 }
460 final Result result = new Result();
461 result.directTls = this.directTls;
462 final int portSegmentStart = hostname.lastIndexOf(':');
463 if (hostname.charAt(hostname.length() - 1) != ']'
464 && portSegmentStart >= 0
465 && hostname.length() >= portSegmentStart + 1) {
466 final String hostPart = hostname.substring(0, portSegmentStart);
467 final String portPart = hostname.substring(portSegmentStart + 1);
468 final Integer port = Ints.tryParse(portPart);
469 if (port == null || Strings.isNullOrEmpty(hostPart)) {
470 return null;
471 }
472 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
473 result.port = port;
474 if (InetAddresses.isInetAddress(host)) {
475 final InetAddress inetAddress;
476 try {
477 inetAddress = InetAddresses.forString(host);
478 } catch (final IllegalArgumentException e) {
479 return null;
480 }
481 result.ip = inetAddress;
482 } else {
483 if (hostPart.trim().isEmpty()) {
484 return null;
485 }
486 try {
487 result.hostname = DnsName.from(hostPart.trim());
488 } catch (final Exception e) {
489 return null;
490 }
491 }
492 } else {
493 final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
494 if (InetAddresses.isInetAddress(host)) {
495 final InetAddress inetAddress;
496 try {
497 inetAddress = InetAddresses.forString(host);
498 } catch (final IllegalArgumentException e) {
499 return null;
500 }
501 result.ip = inetAddress;
502 } else {
503 try {
504 result.hostname = DnsName.from(hostname);
505 } catch (final Exception e) {
506 return null;
507 }
508 }
509 result.port = port;
510 }
511 return result;
512 }
513 }
514}