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