1package eu.siacs.conversations.utils;
2
3import android.content.Context;
4import android.support.annotation.NonNull;
5import android.util.Log;
6
7import java.io.IOException;
8import java.net.Inet4Address;
9import java.net.InetAddress;
10import java.util.ArrayList;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.List;
14
15import de.measite.minidns.DNSClient;
16import de.measite.minidns.DNSName;
17import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
18import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
19import de.measite.minidns.hla.DnssecResolverApi;
20import de.measite.minidns.hla.ResolverApi;
21import de.measite.minidns.hla.ResolverResult;
22import de.measite.minidns.record.A;
23import de.measite.minidns.record.AAAA;
24import de.measite.minidns.record.CNAME;
25import de.measite.minidns.record.Data;
26import de.measite.minidns.record.InternetAddressRR;
27import de.measite.minidns.record.SRV;
28import de.measite.minidns.util.MultipleIoException;
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.R;
31import eu.siacs.conversations.services.XmppConnectionService;
32
33public class Resolver {
34
35 private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
36 private static final String STARTTLS_SERICE = "_xmpp-client";
37
38 private static final String NETWORK_IS_UNREACHABLE = "Network is unreachable";
39
40 private static XmppConnectionService SERVICE = null;
41
42
43 public static void init(XmppConnectionService service) {
44 Resolver.SERVICE = service;
45 DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
46 DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
47 DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
48 }
49
50 public static List<Result> resolve(String domain) throws NetworkIsUnreachableException {
51 List<Result> results = new ArrayList<>();
52 HashSet<String> messages = new HashSet<>();
53 try {
54 results.addAll(resolveSrv(domain, true));
55 } catch (MultipleIoException e) {
56 messages.addAll(extractMessages(e));
57 } catch (Throwable throwable) {
58 Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (direct TLS)",throwable);
59 }
60 try {
61 results.addAll(resolveSrv(domain, false));
62 } catch (MultipleIoException e) {
63 messages.addAll(extractMessages(e));
64 } catch (Throwable throwable) {
65 Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving SRV record (STARTTLS)",throwable);
66 }
67 if (results.size() == 0) {
68 if (messages.size() == 1 && messages.contains(NETWORK_IS_UNREACHABLE)) {
69 throw new NetworkIsUnreachableException();
70 }
71 results.addAll(resolveNoSrvRecords(DNSName.from(domain),true));
72 }
73 Collections.sort(results);
74 Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": "+results.toString());
75 return results;
76 }
77
78 private static HashSet<String> extractMessages(MultipleIoException e) {
79 HashSet<String> messages = new HashSet<>();
80 for(Exception inner : e.getExceptions()) {
81 if (inner instanceof MultipleIoException) {
82 messages.addAll(extractMessages((MultipleIoException) inner));
83 } else {
84 messages.add(inner.getMessage());
85 }
86 }
87 return messages;
88 }
89
90 private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
91 if (Thread.currentThread().isInterrupted()) {
92 return Collections.emptyList();
93 }
94 DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERICE)+"._tcp."+domain);
95 ResolverResult<SRV> result = resolveWithFallback(dnsName,SRV.class);
96 List<Result> results = new ArrayList<>();
97 for(SRV record : result.getAnswersOrEmptySet()) {
98 final boolean addedIPv4 = results.addAll(resolveIp(record,A.class,result.isAuthenticData(),directTls));
99 results.addAll(resolveIp(record,AAAA.class,result.isAuthenticData(),directTls));
100 if (!addedIPv4 && !Thread.currentThread().isInterrupted()) {
101 Result resolverResult = Result.fromRecord(record, directTls);
102 resolverResult.authenticated = resolverResult.isAuthenticated();
103 results.add(resolverResult);
104 }
105 }
106 return results;
107 }
108
109 private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
110 if (Thread.currentThread().isInterrupted()) {
111 return Collections.emptyList();
112 }
113 List<Result> list = new ArrayList<>();
114 try {
115 ResolverResult<D> results = resolveWithFallback(srv.name,type, authenticated);
116 for (D record : results.getAnswersOrEmptySet()) {
117 Result resolverResult = Result.fromRecord(srv, directTls);
118 resolverResult.authenticated = results.isAuthenticData() && authenticated;
119 resolverResult.ip = record.getInetAddress();
120 list.add(resolverResult);
121 }
122 } catch (Throwable t) {
123 Log.d(Config.LOGTAG,Resolver.class.getSimpleName()+": error resolving "+type.getSimpleName()+" "+t.getMessage());
124 }
125 return list;
126 }
127
128 private static List<Result> resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
129 List<Result> results = new ArrayList<>();
130 try {
131 for(A a : resolveWithFallback(dnsName,A.class,false).getAnswersOrEmptySet()) {
132 results.add(Result.createDefault(dnsName,a.getInetAddress()));
133 }
134 for(AAAA aaaa : resolveWithFallback(dnsName,AAAA.class,false).getAnswersOrEmptySet()) {
135 results.add(Result.createDefault(dnsName,aaaa.getInetAddress()));
136 }
137 if (results.size() == 0 && withCnames) {
138 for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
139 results.addAll(resolveNoSrvRecords(cname.name, false));
140 }
141 }
142 } catch (Throwable throwable) {
143 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records",throwable);
144 }
145 results.add(Result.createDefault(dnsName));
146 return results;
147 }
148
149 private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type) throws IOException {
150 return resolveWithFallback(dnsName,type,validateHostname());
151 }
152
153 private static <D extends Data> ResolverResult<D> resolveWithFallback(DNSName dnsName, Class<D> type, boolean validateHostname) throws IOException {
154 if (!validateHostname) {
155 return ResolverApi.INSTANCE.resolve(dnsName, type);
156 }
157 try {
158 final ResolverResult<D> r = DnssecResolverApi.INSTANCE.resolveDnssecReliable(dnsName, type);
159 if (r.wasSuccessful()) {
160 if (r.getAnswers().isEmpty() && type.equals(SRV.class)) {
161 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": resolving SRV records of " + dnsName.toString() + " with DNSSEC yielded empty result");
162 }
163 return r;
164 }
165 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", r.getResolutionUnsuccessfulException());
166 } catch (DNSSECResultNotAuthenticException e) {
167 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
168 } catch (IOException e) {
169 throw e;
170 } catch (Throwable throwable) {
171 Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
172 }
173 return ResolverApi.INSTANCE.resolve(dnsName, type);
174 }
175
176 private static boolean validateHostname() {
177 return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
178 }
179
180 public static class Result implements Comparable<Result> {
181 private InetAddress ip;
182 private DNSName hostname;
183 private int port = 5222;
184 private boolean directTls = false;
185 private boolean authenticated =false;
186 private int priority;
187
188 public InetAddress getIp() {
189 return ip;
190 }
191
192 public int getPort() {
193 return port;
194 }
195
196 public DNSName getHostname() {
197 return hostname;
198 }
199
200 public boolean isDirectTls() {
201 return directTls;
202 }
203
204 public boolean isAuthenticated() {
205 return authenticated;
206 }
207
208 @Override
209 public String toString() {
210 return "Result{" +
211 "ip='" + (ip==null?null:ip.getHostAddress()) + '\'' +
212 ", hostame='" + hostname.toString() + '\'' +
213 ", port=" + port +
214 ", directTls=" + directTls +
215 ", authenticated=" + authenticated +
216 ", priority=" + priority +
217 '}';
218 }
219
220 @Override
221 public int compareTo(@NonNull Result result) {
222 if (result.priority == priority) {
223 if (directTls == result.directTls) {
224 if (ip == null && result.ip == null) {
225 return 0;
226 } else if (ip != null && result.ip != null) {
227 if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
228 return 0;
229 } else {
230 return ip instanceof Inet4Address ? -1 : 1;
231 }
232 } else {
233 return ip != null ? -1 : 1;
234 }
235 } else {
236 return directTls ? -1 : 1;
237 }
238 } else {
239 return priority - result.priority;
240 }
241 }
242
243 public static Result fromRecord(SRV srv, boolean directTls) {
244 Result result = new Result();
245 result.port = srv.port;
246 result.hostname = srv.name;
247 result.directTls = directTls;
248 result.priority = srv.priority;
249 return result;
250 }
251
252 public static Result createDefault(DNSName hostname, InetAddress ip) {
253 Result result = new Result();
254 result.port = 5222;
255 result.hostname = hostname;
256 result.ip = ip;
257 return result;
258 }
259
260 public static Result createDefault(DNSName hostname) {
261 return createDefault(hostname,null);
262 }
263 }
264 public static class NetworkIsUnreachableException extends Exception {
265
266 }
267
268}