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