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