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}