Resolver.java

  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.ArrayList;
 20import java.util.Collections;
 21import java.util.List;
 22
 23//import de.gultsch.minidns.AndroidDNSClient;
 24import org.minidns.AbstractDnsClient;
 25import org.minidns.DnsCache;
 26import org.minidns.DnsClient;
 27import org.minidns.cache.LruCache;
 28import org.minidns.dnsmessage.Question;
 29import org.minidns.dnsname.DnsName;
 30import org.minidns.dnssec.DnssecResultNotAuthenticException;
 31import org.minidns.dnssec.DnssecValidationFailedException;
 32import org.minidns.dnsserverlookup.AndroidUsingExec;
 33import org.minidns.hla.DnssecResolverApi;
 34import org.minidns.hla.ResolverApi;
 35import org.minidns.hla.ResolverResult;
 36import org.minidns.iterative.ReliableDnsClient;
 37import org.minidns.record.A;
 38import org.minidns.record.AAAA;
 39import org.minidns.record.CNAME;
 40import org.minidns.record.Data;
 41import org.minidns.record.InternetAddressRR;
 42import org.minidns.record.Record;
 43import org.minidns.record.SRV;
 44import eu.siacs.conversations.Config;
 45import eu.siacs.conversations.R;
 46import eu.siacs.conversations.services.XmppConnectionService;
 47import eu.siacs.conversations.xmpp.Jid;
 48
 49public class Resolver {
 50
 51    public static final int DEFAULT_PORT_XMPP = 5222;
 52
 53    private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
 54    private static final String STARTTLS_SERVICE = "_xmpp-client";
 55
 56    private static XmppConnectionService SERVICE = null;
 57
 58
 59    public static void init(XmppConnectionService service) {
 60        Resolver.SERVICE = service;
 61        DnsClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
 62        DnsClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
 63        DnsClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
 64        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
 65        if (client instanceof ReliableDnsClient) {
 66            ((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
 67        }
 68    }
 69
 70    public static List<Result> fromHardCoded(final String hostname, final int port) {
 71        final Result result = new Result();
 72        result.hostname = DnsName.from(hostname);
 73        result.port = port;
 74        result.directTls = useDirectTls(port);
 75        result.authenticated = true;
 76        return Collections.singletonList(result);
 77    }
 78
 79    public static void checkDomain(final Jid jid) {
 80        DnsName.from(jid.getDomain());
 81    }
 82
 83    public static boolean invalidHostname(final String hostname) {
 84        try {
 85            DnsName.from(hostname);
 86            return false;
 87        } catch (IllegalArgumentException e) {
 88            return true;
 89        }
 90    }
 91
 92    public static void clearCache() {
 93        final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
 94        final DnsCache dnsCache = client.getCache();
 95        if (dnsCache instanceof LruCache) {
 96            Log.d(Config.LOGTAG,"clearing DNS cache");
 97            ((LruCache) dnsCache).clear();
 98        }
 99    }
100
101
102    public static boolean useDirectTls(final int port) {
103        return port == 443 || port == 5223;
104    }
105
106    public static List<Result> resolve(final String domain) {
107        final  List<Result> ipResults = fromIpAddress(domain);
108        if (ipResults.size() > 0) {
109            return ipResults;
110        }
111        final List<Result> results = new ArrayList<>();
112        final List<Result> fallbackResults = new ArrayList<>();
113        final Thread[] threads = new Thread[3];
114        threads[0] = new Thread(() -> {
115            try {
116                final List<Result> list = resolveSrv(domain, true);
117                synchronized (results) {
118                    results.addAll(list);
119                }
120            } catch (final Throwable throwable) {
121                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
122                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
123                }
124            }
125        });
126        threads[1] = new Thread(() -> {
127            try {
128                final List<Result> list = resolveSrv(domain, false);
129                synchronized (results) {
130                    results.addAll(list);
131                }
132            } catch (final Throwable throwable) {
133                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
134                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
135                }
136            }
137        });
138        threads[2] = new Thread(() -> {
139            List<Result> list = resolveNoSrvRecords(DnsName.from(domain), true);
140            synchronized (fallbackResults) {
141                fallbackResults.addAll(list);
142            }
143        });
144        for (final Thread thread : threads) {
145            thread.start();
146        }
147        try {
148            threads[0].join();
149            threads[1].join();
150            if (results.size() > 0) {
151                threads[2].interrupt();
152                synchronized (results) {
153                    Collections.sort(results);
154                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString());
155                    return new ArrayList<>(results);
156                }
157            } else {
158                threads[2].join();
159                synchronized (fallbackResults) {
160                    Collections.sort(fallbackResults);
161                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString());
162                    return new ArrayList<>(fallbackResults);
163                }
164            }
165        } catch (InterruptedException e) {
166            for (Thread thread : threads) {
167                thread.interrupt();
168            }
169            return Collections.emptyList();
170        }
171    }
172
173    private static List<Result> fromIpAddress(String domain) {
174        if (!IP.matches(domain)) {
175            return Collections.emptyList();
176        }
177        try {
178            Result result = new Result();
179            result.ip = InetAddress.getByName(domain);
180            result.port = DEFAULT_PORT_XMPP;
181            result.authenticated = true;
182            return Collections.singletonList(result);
183        } catch (UnknownHostException e) {
184            return Collections.emptyList();
185        }
186    }
187
188    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
189        DnsName dnsName = DnsName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
190        ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
191        final List<Result> results = new ArrayList<>();
192        final List<Thread> threads = new ArrayList<>();
193        for (SRV record : result.getAnswersOrEmptySet()) {
194            if (record.name.length() == 0 && record.priority == 0) {
195                continue;
196            }
197            threads.add(new Thread(() -> {
198                final List<Result> ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls);
199                if (ipv4s.size() == 0) {
200                    Result resolverResult = Result.fromRecord(record, directTls);
201                    resolverResult.authenticated = result.isAuthenticData();
202                    ipv4s.add(resolverResult);
203                }
204                synchronized (results) {
205                    results.addAll(ipv4s);
206                }
207
208            }));
209            threads.add(new Thread(() -> {
210                final List<Result> ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls);
211                synchronized (results) {
212                    results.addAll(ipv6s);
213                }
214            }));
215        }
216        for (Thread thread : threads) {
217            thread.start();
218        }
219        for (Thread thread : threads) {
220            try {
221                thread.join();
222            } catch (InterruptedException e) {
223                return Collections.emptyList();
224            }
225        }
226        return results;
227    }
228
229    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
230        List<Result> list = new ArrayList<>();
231        try {
232            ResolverResult<D> results = resolveWithFallback(srv.name, type);
233            for (D record : results.getAnswersOrEmptySet()) {
234                Result resolverResult = Result.fromRecord(srv, directTls);
235                resolverResult.authenticated = results.isAuthenticData() && authenticated;
236                resolverResult.ip = record.getInetAddress();
237                list.add(resolverResult);
238            }
239        } catch (Throwable t) {
240            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
241        }
242        return list;
243    }
244
245    private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
246        List<Result> results = new ArrayList<>();
247        try {
248            ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
249            for (A a : aResult.getAnswersOrEmptySet()) {
250                Result r = Result.createDefault(dnsName, a.getInetAddress());
251                r.authenticated = aResult.isAuthenticData();
252                results.add(r);
253            }
254            ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
255            for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
256                Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
257                r.authenticated = aaaaResult.isAuthenticData();
258                results.add(r);
259            }
260            if (results.size() == 0 && withCnames) {
261                ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
262                for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
263                    for (Result r : resolveNoSrvRecords(cname.name, false)) {
264                        r.authenticated = r.authenticated && cnameResult.isAuthenticData();
265                        results.add(r);
266                    }
267                }
268            }
269        } catch (final Throwable throwable) {
270            if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
271                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
272            }
273        }
274        results.add(Result.createDefault(dnsName));
275        return results;
276    }
277
278    private static <D extends Data> ResolverResult<D> resolveWithFallback(DnsName dnsName, Class<D> type) throws IOException {
279        final Question question = new Question(dnsName, Record.TYPE.getType(type));
280        try {
281            ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
282            if (!result.isAuthenticData()) {
283                Log.d(Config.LOGTAG, "DNSSEC validation failed: " + result.getUnverifiedReasons());
284            }
285            return result;
286        } catch (DnssecValidationFailedException e) {
287            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
288        } catch (IOException e) {
289            throw e;
290        } catch (Throwable throwable) {
291            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
292        }
293        return ResolverApi.INSTANCE.resolve(question);
294    }
295
296    public static class Result implements Comparable<Result> {
297        public static final String DOMAIN = "domain";
298        public static final String IP = "ip";
299        public static final String HOSTNAME = "hostname";
300        public static final String PORT = "port";
301        public static final String PRIORITY = "priority";
302        public static final String DIRECT_TLS = "directTls";
303        public static final String AUTHENTICATED = "authenticated";
304        private InetAddress ip;
305        private DnsName hostname;
306        private int port = DEFAULT_PORT_XMPP;
307        private boolean directTls = false;
308        private boolean authenticated = false;
309        private int priority;
310
311        static Result fromRecord(SRV srv, boolean directTls) {
312            Result result = new Result();
313            result.port = srv.port;
314            result.hostname = srv.name;
315            result.directTls = directTls;
316            result.priority = srv.priority;
317            return result;
318        }
319
320        static Result createDefault(DnsName hostname, InetAddress ip) {
321            Result result = new Result();
322            result.port = DEFAULT_PORT_XMPP;
323            result.hostname = hostname;
324            result.ip = ip;
325            return result;
326        }
327
328        static Result createDefault(DnsName hostname) {
329            return createDefault(hostname, null);
330        }
331
332        public static Result fromCursor(Cursor cursor) {
333            final Result result = new Result();
334            try {
335                result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
336            } catch (UnknownHostException e) {
337                result.ip = null;
338            }
339            final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
340            result.hostname = hostname == null ? null : DnsName.from(hostname);
341            result.port = cursor.getInt(cursor.getColumnIndex(PORT));
342            result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
343            result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
344            result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
345            return result;
346        }
347
348        @Override
349        public boolean equals(Object o) {
350            if (this == o) return true;
351            if (o == null || getClass() != o.getClass()) return false;
352
353            Result result = (Result) o;
354
355            if (port != result.port) return false;
356            if (directTls != result.directTls) return false;
357            if (authenticated != result.authenticated) return false;
358            if (priority != result.priority) return false;
359            if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
360            return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
361        }
362
363        @Override
364        public int hashCode() {
365            int result = ip != null ? ip.hashCode() : 0;
366            result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
367            result = 31 * result + port;
368            result = 31 * result + (directTls ? 1 : 0);
369            result = 31 * result + (authenticated ? 1 : 0);
370            result = 31 * result + priority;
371            return result;
372        }
373
374        public InetAddress getIp() {
375            return ip;
376        }
377
378        public int getPort() {
379            return port;
380        }
381
382        public DnsName getHostname() {
383            return hostname;
384        }
385
386        public boolean isDirectTls() {
387            return directTls;
388        }
389
390        public boolean isAuthenticated() {
391            return authenticated;
392        }
393
394        @Override
395        public String toString() {
396            return "Result{" +
397                    "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
398                    ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' +
399                    ", port=" + port +
400                    ", directTls=" + directTls +
401                    ", authenticated=" + authenticated +
402                    ", priority=" + priority +
403                    '}';
404        }
405
406        @Override
407        public int compareTo(@NonNull Result result) {
408            if (result.priority == priority) {
409                if (directTls == result.directTls) {
410                    if (ip == null && result.ip == null) {
411                        return 0;
412                    } else if (ip != null && result.ip != null) {
413                        if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
414                            return 0;
415                        } else {
416                            return ip instanceof Inet4Address ? -1 : 1;
417                        }
418                    } else {
419                        return ip != null ? -1 : 1;
420                    }
421                } else {
422                    return directTls ? -1 : 1;
423                }
424            } else {
425                return priority - result.priority;
426            }
427        }
428
429        public ContentValues toContentValues() {
430            final ContentValues contentValues = new ContentValues();
431            contentValues.put(IP, ip == null ? null : ip.getAddress());
432            contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString());
433            contentValues.put(PORT, port);
434            contentValues.put(PRIORITY, priority);
435            contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
436            contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
437            return contentValues;
438        }
439
440        public Result seeOtherHost(final String seeOtherHost) {
441            final String hostname = seeOtherHost.trim();
442            if (hostname.isEmpty()) {
443                return null;
444            }
445            final Result result = new Result();
446            result.directTls = this.directTls;
447            final int portSegmentStart = hostname.lastIndexOf(':');
448            if (hostname.charAt(hostname.length() - 1) != ']'
449                    && portSegmentStart >= 0
450                    && hostname.length() >= portSegmentStart + 1) {
451                final String hostPart = hostname.substring(0, portSegmentStart);
452                final String portPart = hostname.substring(portSegmentStart + 1);
453                final Integer port = Ints.tryParse(portPart);
454                if (port == null || Strings.isNullOrEmpty(hostPart)) {
455                    return null;
456                }
457                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
458                result.port = port;
459                if (InetAddresses.isInetAddress(host)) {
460                    final InetAddress inetAddress;
461                    try {
462                        inetAddress = InetAddresses.forString(host);
463                    } catch (final IllegalArgumentException e) {
464                        return null;
465                    }
466                    result.ip = inetAddress;
467                } else {
468                    if (hostPart.trim().isEmpty()) {
469                        return null;
470                    }
471                    try {
472                        result.hostname = DnsName.from(hostPart.trim());
473                    } catch (final Exception e) {
474                        return null;
475                    }
476                }
477            } else {
478                final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
479                if (InetAddresses.isInetAddress(host)) {
480                    final InetAddress inetAddress;
481                    try {
482                        inetAddress = InetAddresses.forString(host);
483                    } catch (final IllegalArgumentException e) {
484                        return null;
485                    }
486                    result.ip = inetAddress;
487                } else {
488                    try {
489                        result.hostname = DnsName.from(hostname);
490                    } catch (final Exception e) {
491                        return null;
492                    }
493                }
494                result.port = port;
495            }
496            return result;
497        }
498    }
499
500}