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