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