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			if (record.name.length() == 0 && record.priority == 0) {
103				continue;
104			}
105			final boolean addedIPv4 = results.addAll(resolveIp(record, A.class, result.isAuthenticData(), directTls));
106			results.addAll(resolveIp(record, AAAA.class, result.isAuthenticData(), directTls));
107			if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
108				Result resolverResult = Result.fromRecord(record, directTls);
109				resolverResult.authenticated = resolverResult.isAuthenticated();
110				results.add(resolverResult);
111			}
112		}
113		return results;
114	}
115
116	private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
117		if (Thread.currentThread().isInterrupted()) {
118			return Collections.emptyList();
119		}
120		List<Result> list = new ArrayList<>();
121		try {
122			ResolverResult<D> results = resolveWithFallback(srv.name, type, authenticated);
123			for (D record : results.getAnswersOrEmptySet()) {
124				Result resolverResult = Result.fromRecord(srv, directTls);
125				resolverResult.authenticated = results.isAuthenticData() && authenticated;
126				resolverResult.ip = record.getInetAddress();
127				list.add(resolverResult);
128			}
129		} catch (Throwable t) {
130			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
131		}
132		return list;
133	}
134
135	private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
136		List<Result> results = new ArrayList<>();
137		try {
138			for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) {
139				results.add(Result.createDefault(dnsName, a.getInetAddress()));
140			}
141			for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) {
142				results.add(Result.createDefault(dnsName, aaaa.getInetAddress()));
143			}
144			if (results.size() == 0 && withCnames) {
145				for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
146					results.addAll(resolveNoSrvRecords(cname.name, false));
147				}
148			}
149		} catch (Throwable throwable) {
150			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
151		}
152		results.add(Result.createDefault(dnsName));
153		return results;
154	}
155
156	private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
157		return resolveWithFallback(dnsName, type, validateHostname());
158	}
159
160	private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
161		final Question question = new Question(dnsName, Record.TYPE.getType(type));
162		if (!validateHostname) {
163			return ResolverApi.INSTANCE.resolve(question);
164		}
165		try {
166			return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
167		} catch (DNSSECResultNotAuthenticException e) {
168			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
169		} catch (IOException e) {
170			throw e;
171		} catch (Throwable throwable) {
172			Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
173		}
174		return ResolverApi.INSTANCE.resolve(question);
175	}
176
177	private static boolean validateHostname() {
178		return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
179	}
180
181	public static class Result implements Comparable<Result> {
182		@Override
183		public boolean equals(Object o) {
184			if (this == o) return true;
185			if (o == null || getClass() != o.getClass()) return false;
186
187			Result result = (Result) o;
188
189			if (port != result.port) return false;
190			if (directTls != result.directTls) return false;
191			if (authenticated != result.authenticated) return false;
192			if (priority != result.priority) return false;
193			if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
194			return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
195		}
196
197		@Override
198		public int hashCode() {
199			int result = ip != null ? ip.hashCode() : 0;
200			result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
201			result = 31 * result + port;
202			result = 31 * result + (directTls ? 1 : 0);
203			result = 31 * result + (authenticated ? 1 : 0);
204			result = 31 * result + priority;
205			return result;
206		}
207
208		public static final String DOMAIN = "domain";
209
210		public static final String IP = "ip";
211		public static final String HOSTNAME = "hostname";
212		public static final String PORT = "port";
213		public static final String PRIORITY = "priority";
214		public static final String DIRECT_TLS = "directTls";
215		public static final String AUTHENTICATED = "authenticated";
216
217		private InetAddress ip;
218		private DNSName hostname;
219		private int port = 5222;
220		private boolean directTls = false;
221		private boolean authenticated = false;
222		private int priority;
223
224		public InetAddress getIp() {
225			return ip;
226		}
227
228		public int getPort() {
229			return port;
230		}
231
232		public DNSName getHostname() {
233			return hostname;
234		}
235
236		public boolean isDirectTls() {
237			return directTls;
238		}
239
240		public boolean isAuthenticated() {
241			return authenticated;
242		}
243
244		@Override
245		public String toString() {
246			return "Result{" +
247					"ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
248					", hostame='" + hostname.toString() + '\'' +
249					", port=" + port +
250					", directTls=" + directTls +
251					", authenticated=" + authenticated +
252					", priority=" + priority +
253					'}';
254		}
255
256		@Override
257		public int compareTo(@NonNull Result result) {
258			if (result.priority == priority) {
259				if (directTls == result.directTls) {
260					if (ip == null && result.ip == null) {
261						return 0;
262					} else if (ip != null && result.ip != null) {
263						if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
264							return 0;
265						} else {
266							return ip instanceof Inet4Address ? -1 : 1;
267						}
268					} else {
269						return ip != null ? -1 : 1;
270					}
271				} else {
272					return directTls ? -1 : 1;
273				}
274			} else {
275				return priority - result.priority;
276			}
277		}
278
279		public static Result fromRecord(SRV srv, boolean directTls) {
280			Result result = new Result();
281			result.port = srv.port;
282			result.hostname = srv.name;
283			result.directTls = directTls;
284			result.priority = srv.priority;
285			return result;
286		}
287
288		public static Result createDefault(DNSName hostname, InetAddress ip) {
289			Result result = new Result();
290			result.port = 5222;
291			result.hostname = hostname;
292			result.ip = ip;
293			return result;
294		}
295
296		public static Result createDefault(DNSName hostname) {
297			return createDefault(hostname, null);
298		}
299
300		public static Result fromCursor(Cursor cursor) {
301			final Result result = new Result();
302			try {
303				result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
304			} catch (UnknownHostException e) {
305				result.ip = null;
306			}
307			result.hostname = DNSName.from(cursor.getString(cursor.getColumnIndex(HOSTNAME)));
308			result.port = cursor.getInt(cursor.getColumnIndex(PORT));
309			result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
310			result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
311			result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
312			return result;
313		}
314
315		public ContentValues toContentValues() {
316			final ContentValues contentValues = new ContentValues();
317			contentValues.put(IP, ip == null ? null : ip.getAddress());
318			contentValues.put(HOSTNAME, hostname.toString());
319			contentValues.put(PORT, port);
320			contentValues.put(PRIORITY, priority);
321			contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
322			contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
323			return contentValues;
324		}
325	}
326
327	public static class NetworkIsUnreachableException extends Exception {
328
329	}
330
331}