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 final boolean addedIPv4 = results.addAll(resolveIp(record, A.class, result.isAuthenticData(), directTls));
103 results.addAll(resolveIp(record, AAAA.class, result.isAuthenticData(), directTls));
104 if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
105 Result resolverResult = Result.fromRecord(record, directTls);
106 resolverResult.authenticated = resolverResult.isAuthenticated();
107 results.add(resolverResult);
108 }
109 }
110 return results;
111 }
112
113 private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
114 if (Thread.currentThread().isInterrupted()) {
115 return Collections.emptyList();
116 }
117 List<Result> list = new ArrayList<>();
118 try {
119 ResolverResult<D> results = resolveWithFallback(srv.name, type, authenticated);
120 for (D record : results.getAnswersOrEmptySet()) {
121 Result resolverResult = Result.fromRecord(srv, directTls);
122 resolverResult.authenticated = results.isAuthenticData() && authenticated;
123 resolverResult.ip = record.getInetAddress();
124 list.add(resolverResult);
125 }
126 } catch (Throwable t) {
127 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
128 }
129 return list;
130 }
131
132 private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
133 List<Result> results = new ArrayList<>();
134 try {
135 for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) {
136 results.add(Result.createDefault(dnsName, a.getInetAddress()));
137 }
138 for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) {
139 results.add(Result.createDefault(dnsName, aaaa.getInetAddress()));
140 }
141 if (results.size() == 0 && withCnames) {
142 for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
143 results.addAll(resolveNoSrvRecords(cname.name, false));
144 }
145 }
146 } catch (Throwable throwable) {
147 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
148 }
149 results.add(Result.createDefault(dnsName));
150 return results;
151 }
152
153 private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
154 return resolveWithFallback(dnsName, type, validateHostname());
155 }
156
157 private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
158 final Question question = new Question(dnsName, Record.TYPE.getType(type));
159 if (!validateHostname) {
160 return ResolverApi.INSTANCE.resolve(question);
161 }
162 try {
163 return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
164 } catch (DNSSECResultNotAuthenticException e) {
165 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
166 } catch (IOException e) {
167 throw e;
168 } catch (Throwable throwable) {
169 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
170 }
171 return ResolverApi.INSTANCE.resolve(question);
172 }
173
174 private static boolean validateHostname() {
175 return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
176 }
177
178 public static class Result implements Comparable<Result> {
179 @Override
180 public boolean equals(Object o) {
181 if (this == o) return true;
182 if (o == null || getClass() != o.getClass()) return false;
183
184 Result result = (Result) o;
185
186 if (port != result.port) return false;
187 if (directTls != result.directTls) return false;
188 if (authenticated != result.authenticated) return false;
189 if (priority != result.priority) return false;
190 if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
191 return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
192 }
193
194 @Override
195 public int hashCode() {
196 int result = ip != null ? ip.hashCode() : 0;
197 result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
198 result = 31 * result + port;
199 result = 31 * result + (directTls ? 1 : 0);
200 result = 31 * result + (authenticated ? 1 : 0);
201 result = 31 * result + priority;
202 return result;
203 }
204
205 public static final String DOMAIN = "domain";
206
207 public static final String IP = "ip";
208 public static final String HOSTNAME = "hostname";
209 public static final String PORT = "port";
210 public static final String PRIORITY = "priority";
211 public static final String DIRECT_TLS = "directTls";
212 public static final String AUTHENTICATED = "authenticated";
213
214 private InetAddress ip;
215 private DNSName hostname;
216 private int port = 5222;
217 private boolean directTls = false;
218 private boolean authenticated = false;
219 private int priority;
220
221 public InetAddress getIp() {
222 return ip;
223 }
224
225 public int getPort() {
226 return port;
227 }
228
229 public DNSName getHostname() {
230 return hostname;
231 }
232
233 public boolean isDirectTls() {
234 return directTls;
235 }
236
237 public boolean isAuthenticated() {
238 return authenticated;
239 }
240
241 @Override
242 public String toString() {
243 return "Result{" +
244 "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
245 ", hostame='" + hostname.toString() + '\'' +
246 ", port=" + port +
247 ", directTls=" + directTls +
248 ", authenticated=" + authenticated +
249 ", priority=" + priority +
250 '}';
251 }
252
253 @Override
254 public int compareTo(@NonNull Result result) {
255 if (result.priority == priority) {
256 if (directTls == result.directTls) {
257 if (ip == null && result.ip == null) {
258 return 0;
259 } else if (ip != null && result.ip != null) {
260 if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
261 return 0;
262 } else {
263 return ip instanceof Inet4Address ? -1 : 1;
264 }
265 } else {
266 return ip != null ? -1 : 1;
267 }
268 } else {
269 return directTls ? -1 : 1;
270 }
271 } else {
272 return priority - result.priority;
273 }
274 }
275
276 public static Result fromRecord(SRV srv, boolean directTls) {
277 Result result = new Result();
278 result.port = srv.port;
279 result.hostname = srv.name;
280 result.directTls = directTls;
281 result.priority = srv.priority;
282 return result;
283 }
284
285 public static Result createDefault(DNSName hostname, InetAddress ip) {
286 Result result = new Result();
287 result.port = 5222;
288 result.hostname = hostname;
289 result.ip = ip;
290 return result;
291 }
292
293 public static Result createDefault(DNSName hostname) {
294 return createDefault(hostname, null);
295 }
296
297 public static Result fromCursor(Cursor cursor) {
298 final Result result = new Result();
299 try {
300 result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
301 } catch (UnknownHostException e) {
302 result.ip = null;
303 }
304 result.hostname = DNSName.from(cursor.getString(cursor.getColumnIndex(HOSTNAME)));
305 result.port = cursor.getInt(cursor.getColumnIndex(PORT));
306 result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
307 result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
308 result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
309 return result;
310 }
311
312 public ContentValues toContentValues() {
313 final ContentValues contentValues = new ContentValues();
314 contentValues.put(IP, ip == null ? null : ip.getAddress());
315 contentValues.put(HOSTNAME, hostname.toString());
316 contentValues.put(PORT, port);
317 contentValues.put(PRIORITY, priority);
318 contentValues.put(DIRECT_TLS, directTls ? 1 : 0);
319 contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
320 return contentValues;
321 }
322 }
323
324 public static class NetworkIsUnreachableException extends Exception {
325
326 }
327
328}