Resolver.java

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