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