Resolver.java

  1package eu.siacs.conversations.utils;
  2
  3import android.content.Context;
  4import android.support.annotation.NonNull;
  5import android.util.Log;
  6
  7import java.io.IOException;
  8import java.net.Inet4Address;
  9import java.net.InetAddress;
 10import java.util.ArrayList;
 11import java.util.Collections;
 12import java.util.HashSet;
 13import java.util.List;
 14
 15import de.measite.minidns.DNSClient;
 16import de.measite.minidns.DNSName;
 17import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
 18import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
 19import de.measite.minidns.hla.DnssecResolverApi;
 20import de.measite.minidns.hla.ResolverApi;
 21import de.measite.minidns.hla.ResolverResult;
 22import de.measite.minidns.record.A;
 23import de.measite.minidns.record.AAAA;
 24import de.measite.minidns.record.CNAME;
 25import de.measite.minidns.record.Data;
 26import de.measite.minidns.record.InternetAddressRR;
 27import de.measite.minidns.record.SRV;
 28import de.measite.minidns.util.MultipleIoException;
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.R;
 31import eu.siacs.conversations.services.XmppConnectionService;
 32
 33public class Resolver {
 34
 35    private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
 36    private static final String STARTTLS_SERICE = "_xmpp-client";
 37
 38    private static final String NETWORK_IS_UNREACHABLE = "Network is unreachable";
 39
 40    private static XmppConnectionService SERVICE = null;
 41
 42
 43    public static void init(XmppConnectionService service) {
 44        Resolver.SERVICE = service;
 45        DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
 46        DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
 47        DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
 48    }
 49
 50    public static List<Result> resolve(String domain) throws NetworkIsUnreachableException {
 51        List<Result> results = new ArrayList<>();
 52        HashSet<String> messages = new HashSet<>();
 53        try {
 54            results.addAll(resolveSrv(domain, true));
 55        } catch (MultipleIoException e) {
 56            messages.addAll(extractMessages(e));
 57        } catch (Throwable throwable) {
 58            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (direct TLS)",throwable);
 59        }
 60        try {
 61            results.addAll(resolveSrv(domain, false));
 62        } catch (MultipleIoException e) {
 63            messages.addAll(extractMessages(e));
 64        } catch (Throwable throwable) {
 65            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (STARTTLS)",throwable);
 66        }
 67        if (results.size() == 0) {
 68            if (messages.size() == 1 && messages.contains(NETWORK_IS_UNREACHABLE)) {
 69                throw new NetworkIsUnreachableException();
 70            }
 71            results.addAll(resolveNoSrvRecords(DNSName.from(domain),true));
 72        }
 73        Collections.sort(results);
 74        Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": "+results.toString());
 75        return results;
 76    }
 77
 78    private static HashSet<String> extractMessages(MultipleIoException e) {
 79        HashSet<String> messages = new HashSet<>();
 80        for(Exception inner : e.getExceptions()) {
 81            if (inner instanceof MultipleIoException) {
 82                messages.addAll(extractMessages((MultipleIoException) inner));
 83            } else {
 84                messages.add(inner.getMessage());
 85            }
 86        }
 87        return messages;
 88    }
 89
 90    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
 91        if (Thread.currentThread().isInterrupted()) {
 92            return Collections.emptyList();
 93        }
 94        DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERICE)+"._tcp."+domain);
 95        ResolverResult<SRV> result = resolveWithFallback(dnsName,SRV.class);
 96        List<Result> results = new ArrayList<>();
 97        for(SRV record : result.getAnswersOrEmptySet()) {
 98            final boolean addedIPv4 = results.addAll(resolveIp(record,A.class,result.isAuthenticData(),directTls));
 99            results.addAll(resolveIp(record,AAAA.class,result.isAuthenticData(),directTls));
100            if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
101                Result resolverResult = Result.fromRecord(record, directTls);
102                resolverResult.authenticated = resolverResult.isAuthenticated();
103                results.add(resolverResult);
104            }
105        }
106        return results;
107    }
108
109    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
110        if (Thread.currentThread().isInterrupted()) {
111            return Collections.emptyList();
112        }
113        List<Result> list = new ArrayList<>();
114        try {
115            ResolverResult<D> results = resolveWithFallback(srv.name,type, authenticated);
116            for (D record : results.getAnswersOrEmptySet()) {
117                Result resolverResult = Result.fromRecord(srv, directTls);
118                resolverResult.authenticated = results.isAuthenticData() && authenticated;
119                resolverResult.ip = record.getInetAddress();
120                list.add(resolverResult);
121            }
122        } catch (Throwable t) {
123            Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving "+type.getSimpleName()+" "+t.getMessage());
124        }
125        return list;
126    }
127
128    private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
129        List<Result> results = new ArrayList<>();
130        try {
131            for(A a : resolveWithFallback(dnsName,A.class,false).getAnswersOrEmptySet()) {
132                results.add(Result.createDefault(dnsName,a.getInetAddress()));
133            }
134            for(AAAA aaaa : resolveWithFallback(dnsName,AAAA.class,false).getAnswersOrEmptySet()) {
135                results.add(Result.createDefault(dnsName,aaaa.getInetAddress()));
136            }
137            if (results.size() == 0 && withCnames) {
138                for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
139                    results.addAll(resolveNoSrvRecords(cname.name, false));
140                }
141            }
142        } catch (Throwable throwable) {
143            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records",throwable);
144        }
145        results.add(Result.createDefault(dnsName));
146        return results;
147    }
148
149    private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
150        return resolveWithFallback(dnsName,type,validateHostname());
151    }
152
153    private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
154        if (!validateHostname) {
155            return ResolverApi.INSTANCE.resolve(dnsName, type);
156        }
157        try {
158            final ResolverResult<D> r = DnssecResolverApi.INSTANCE.resolveDnssecReliable(dnsName, type);
159            if (r.wasSuccessful()) {
160                if (r.getAnswers().isEmpty() && type.equals(SRV.class)) {
161                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": resolving  SRV records of " + dnsName.toString() + " with DNSSEC yielded empty result");
162                }
163                return r;
164            }
165            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", r.getResolutionUnsuccessfulException());
166        } catch (DNSSECResultNotAuthenticException e) {
167            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
168        } catch (IOException e) {
169            throw e;
170        } catch (Throwable throwable) {
171            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
172        }
173        return ResolverApi.INSTANCE.resolve(dnsName, type);
174    }
175
176    private static boolean validateHostname() {
177        return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
178    }
179
180    public static class Result implements Comparable<Result> {
181        private InetAddress ip;
182        private DNSName hostname;
183        private int port = 5222;
184        private boolean directTls = false;
185        private boolean authenticated =false;
186        private int priority;
187
188        public InetAddress getIp() {
189            return ip;
190        }
191
192        public int getPort() {
193            return port;
194        }
195
196        public DNSName getHostname() {
197            return hostname;
198        }
199
200        public boolean isDirectTls() {
201            return directTls;
202        }
203
204        public boolean isAuthenticated() {
205            return authenticated;
206        }
207
208        @Override
209        public String toString() {
210            return "Result{" +
211                    "ip='" + (ip==null?null:ip.getHostAddress()) + '\'' +
212                    ", hostame='" + hostname.toString() + '\'' +
213                    ", port=" + port +
214                    ", directTls=" + directTls +
215                    ", authenticated=" + authenticated +
216                    ", priority=" + priority +
217                    '}';
218        }
219
220        @Override
221        public int compareTo(@NonNull Result result) {
222            if (result.priority == priority) {
223                if (directTls == result.directTls) {
224                    if (ip == null && result.ip == null) {
225                        return 0;
226                    } else if (ip != null && result.ip != null) {
227                        if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
228                            return 0;
229                        } else {
230                            return ip instanceof Inet4Address ? -1 : 1;
231                        }
232                    } else {
233                        return ip != null ? -1 : 1;
234                    }
235                } else {
236                    return directTls ? -1 : 1;
237                }
238            } else {
239                return priority - result.priority;
240            }
241        }
242
243        public static Result fromRecord(SRV srv, boolean directTls) {
244            Result result = new Result();
245            result.port = srv.port;
246            result.hostname = srv.name;
247            result.directTls = directTls;
248            result.priority = srv.priority;
249            return result;
250        }
251
252        public static Result createDefault(DNSName hostname, InetAddress ip) {
253            Result result = new Result();
254            result.port = 5222;
255            result.hostname = hostname;
256            result.ip = ip;
257            return result;
258        }
259
260        public static Result createDefault(DNSName hostname) {
261            return createDefault(hostname,null);
262        }
263    }
264    public static class NetworkIsUnreachableException extends Exception {
265
266    }
267
268}