Resolver.java

  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}