1package de.measite.minidns;
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.io.InputStreamReader;
6import java.io.LineNumberReader;
7import java.lang.reflect.Method;
8import java.net.DatagramPacket;
9import java.net.DatagramSocket;
10import java.net.InetAddress;
11import java.security.NoSuchAlgorithmException;
12import java.security.SecureRandom;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.HashSet;
16import java.util.LinkedHashMap;
17import java.util.Map.Entry;
18import java.util.Random;
19import java.util.logging.Level;
20import java.util.logging.Logger;
21
22import de.measite.minidns.Record.CLASS;
23import de.measite.minidns.Record.TYPE;
24
25/**
26 * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
27 * This circumvents the missing javax.naming package on android.
28 */
29public class Client {
30
31 private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
32
33 /**
34 * The internal random class for sequence generation.
35 */
36 protected Random random;
37
38 /**
39 * The buffer size for dns replies.
40 */
41 protected int bufferSize = 1500;
42
43 /**
44 * DNS timeout.
45 */
46 protected int timeout = 5000;
47
48 /**
49 * The internal DNS cache.
50 */
51 protected LinkedHashMap<Question, DNSMessage> cache;
52
53 /**
54 * Maximum acceptable ttl.
55 */
56 protected long maxTTL = 60 * 60 * 1000;
57
58 /**
59 * Create a new DNS client.
60 */
61 public Client() {
62 try {
63 random = SecureRandom.getInstance("SHA1PRNG");
64 } catch (NoSuchAlgorithmException e1) {
65 random = new SecureRandom();
66 }
67 setCacheSize(10);
68 }
69
70 /**
71 * Query a nameserver for a single entry.
72 * @param name The DNS name to request.
73 * @param type The DNS type to request (SRV, A, AAAA, ...).
74 * @param clazz The class of the request (usually IN for Internet).
75 * @param host The DNS server host.
76 * @param port The DNS server port.
77 * @return
78 * @throws IOException On IO Errors.
79 */
80 public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port)
81 throws IOException
82 {
83 Question q = new Question(name, type, clazz);
84 return query(q, host, port);
85 }
86
87 /**
88 * Query a nameserver for a single entry.
89 * @param name The DNS name to request.
90 * @param type The DNS type to request (SRV, A, AAAA, ...).
91 * @param clazz The class of the request (usually IN for Internet).
92 * @param host The DNS server host.
93 * @return
94 * @throws IOException On IO Errors.
95 */
96 public DNSMessage query(String name, TYPE type, CLASS clazz, String host)
97 throws IOException
98 {
99 Question q = new Question(name, type, clazz);
100 return query(q, host);
101 }
102
103 /**
104 * Query the system nameserver for a single entry.
105 * @param name The DNS name to request.
106 * @param type The DNS type to request (SRV, A, AAAA, ...).
107 * @param clazz The class of the request (usually IN for Internet).
108 * @return The DNSMessage reply or null.
109 */
110 public DNSMessage query(String name, TYPE type, CLASS clazz)
111 {
112 Question q = new Question(name, type, clazz);
113 return query(q);
114 }
115
116 /**
117 * Query a specific server for one entry.
118 * @param q The question section of the DNS query.
119 * @param host The dns server host.
120 * @throws IOException On IOErrors.
121 */
122 public DNSMessage query(Question q, String host) throws IOException {
123 return query(q, host, 53);
124 }
125
126 /**
127 * Query a specific server for one entry.
128 * @param q The question section of the DNS query.
129 * @param host The dns server host.
130 * @param port the dns port.
131 * @throws IOException On IOErrors.
132 */
133 public DNSMessage query(Question q, String host, int port) throws IOException {
134 DNSMessage dnsMessage = (cache == null) ? null : cache.get(q);
135 if (dnsMessage != null && dnsMessage.getReceiveTimestamp() > 0l) {
136 // check the ttl
137 long ttl = maxTTL;
138 for (Record r : dnsMessage.getAnswers()) {
139 ttl = Math.min(ttl, r.ttl);
140 }
141 for (Record r : dnsMessage.getAdditionalResourceRecords()) {
142 ttl = Math.min(ttl, r.ttl);
143 }
144 if (dnsMessage.getReceiveTimestamp() + ttl <
145 System.currentTimeMillis()) {
146 return dnsMessage;
147 }
148 }
149 DNSMessage message = new DNSMessage();
150 message.setQuestions(new Question[]{q});
151 message.setRecursionDesired(true);
152 message.setId(random.nextInt());
153 byte[] buf = message.toArray();
154 try (DatagramSocket socket = new DatagramSocket()) {
155 DatagramPacket packet = new DatagramPacket(buf, buf.length,
156 InetAddress.getByName(host), port);
157 socket.setSoTimeout(timeout);
158 socket.send(packet);
159 packet = new DatagramPacket(new byte[bufferSize], bufferSize);
160 socket.receive(packet);
161 dnsMessage = DNSMessage.parse(packet.getData());
162 if (dnsMessage.getId() != message.getId()) {
163 return null;
164 }
165 for (Record record : dnsMessage.getAnswers()) {
166 if (record.isAnswer(q)) {
167 if (cache != null) {
168 cache.put(q, dnsMessage);
169 }
170 break;
171 }
172 }
173 return dnsMessage;
174 }
175 }
176
177 /**
178 * Query the system DNS server for one entry.
179 * @param q The question section of the DNS query.
180 */
181 public DNSMessage query(Question q) {
182 // While this query method does in fact re-use query(Question, String)
183 // we still do a cache lookup here in order to avoid unnecessary
184 // findDNS()calls, which are expensive on Android. Note that we do not
185 // put the results back into the Cache, as this is already done by
186 // query(Question, String).
187 DNSMessage message = cache.get(q);
188 if (message != null) {
189 return message;
190 }
191 String dnsServer[] = findDNS();
192 for (String dns : dnsServer) {
193 try {
194 message = query(q, dns);
195 if (message == null) {
196 continue;
197 }
198 if (message.getResponseCode() !=
199 DNSMessage.RESPONSE_CODE.NO_ERROR) {
200 continue;
201 }
202 for (Record record: message.getAnswers()) {
203 if (record.isAnswer(q)) {
204 return message;
205 }
206 }
207 } catch (IOException ioe) {
208 LOGGER.log(Level.FINE, "IOException in query", ioe);
209 }
210 }
211 return null;
212 }
213
214 /**
215 * Retrieve a list of currently configured DNS servers.
216 * @return The server array.
217 */
218 public String[] findDNS() {
219 String[] result = findDNSByReflection();
220 if (result != null) {
221 LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result));
222 return result;
223 }
224
225 result = findDNSByExec();
226 if (result != null) {
227 LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result));
228 return result;
229 }
230
231 // fallback for ipv4 and ipv6 connectivity
232 // see https://developers.google.com/speed/public-dns/docs/using
233 LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]");
234
235 return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"};
236 }
237
238 /**
239 * Try to retrieve the list of dns server by executing getprop.
240 * @return Array of servers, or null on failure.
241 */
242 protected String[] findDNSByExec() {
243 try {
244 Process process = Runtime.getRuntime().exec("getprop");
245 InputStream inputStream = process.getInputStream();
246 LineNumberReader lnr = new LineNumberReader(
247 new InputStreamReader(inputStream));
248 String line = null;
249 HashSet<String> server = new HashSet<String>(6);
250 while ((line = lnr.readLine()) != null) {
251 int split = line.indexOf("]: [");
252 if (split == -1) {
253 continue;
254 }
255 String property = line.substring(1, split);
256 String value = line.substring(split + 4, line.length() - 1);
257 if (property.endsWith(".dns") || property.endsWith(".dns1") ||
258 property.endsWith(".dns2") || property.endsWith(".dns3") ||
259 property.endsWith(".dns4")) {
260
261 // normalize the address
262
263 InetAddress ip = InetAddress.getByName(value);
264
265 if (ip == null) continue;
266
267 value = ip.getHostAddress();
268
269 if (value == null) continue;
270 if (value.length() == 0) continue;
271
272 server.add(value);
273 }
274 }
275 if (server.size() > 0) {
276 return server.toArray(new String[server.size()]);
277 }
278 } catch (IOException e) {
279 LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
280 }
281 return null;
282 }
283
284 /**
285 * Try to retrieve the list of dns server by calling SystemProperties.
286 * @return Array of servers, or null on failure.
287 */
288 protected String[] findDNSByReflection() {
289 try {
290 Class<?> SystemProperties =
291 Class.forName("android.os.SystemProperties");
292 Method method = SystemProperties.getMethod("get",
293 new Class[] { String.class });
294
295 ArrayList<String> servers = new ArrayList<String>(5);
296
297 for (String propKey : new String[] {
298 "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
299
300 String value = (String)method.invoke(null, propKey);
301
302 if (value == null) continue;
303 if (value.length() == 0) continue;
304 if (servers.contains(value)) continue;
305
306 InetAddress ip = InetAddress.getByName(value);
307
308 if (ip == null) continue;
309
310 value = ip.getHostAddress();
311
312 if (value == null) continue;
313 if (value.length() == 0) continue;
314 if (servers.contains(value)) continue;
315
316 servers.add(value);
317 }
318
319 if (servers.size() > 0) {
320 return servers.toArray(new String[servers.size()]);
321 }
322 } catch (Exception e) {
323 // we might trigger some problems this way
324 LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
325 }
326 return null;
327 }
328
329 /**
330 * Configure the cache size (default 10).
331 * @param maximumSize The new cache size or 0 to disable.
332 */
333 @SuppressWarnings("serial")
334 public void setCacheSize(final int maximumSize) {
335 if (maximumSize == 0) {
336 this.cache = null;
337 } else {
338 LinkedHashMap<Question,DNSMessage> old = cache;
339 cache = new LinkedHashMap<Question,DNSMessage>() {
340 @Override
341 protected boolean removeEldestEntry(
342 Entry<Question, DNSMessage> eldest) {
343 return size() > maximumSize;
344 }
345 };
346 if (old != null) {
347 cache.putAll(old);
348 }
349 }
350 }
351
352 /**
353 * Flush the DNS cache.
354 */
355 public void flushCache() {
356 if (cache != null) {
357 cache.clear();
358 }
359 }
360
361 /**
362 * Get the current maximum record ttl.
363 * @return The maximum record ttl.
364 */
365 public long getMaxTTL() {
366 return maxTTL;
367 }
368
369 /**
370 * Set the maximum record ttl.
371 * @param maxTTL The new maximum ttl.
372 */
373 public void setMaxTTL(long maxTTL) {
374 this.maxTTL = maxTTL;
375 }
376
377}