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