Client.java

  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}