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 The response (or null on timeout / failure).
 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 The response (or null on timeout / failure).
 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 response (or null on timeout/error).
110     * @return The DNSMessage reply or null.
111     */
112    public DNSMessage query(String name, TYPE type, CLASS clazz)
113    {
114        Question q = new Question(name, type, clazz);
115        return query(q);
116    }
117
118    /**
119     * Query a specific server for one entry.
120     * @param q The question section of the DNS query.
121     * @param host The dns server host.
122     * @return The response (or null on timeout/error).
123     * @throws IOException On IOErrors.
124     */
125    public DNSMessage query(Question q, String host) throws IOException {
126        return query(q, host, 53);
127    }
128
129    /**
130     * Query a specific server for one entry.
131     * @param q The question section of the DNS query.
132     * @param host The dns server host.
133     * @param port the dns port.
134     * @return The response (or null on timeout/error).
135     * @throws IOException On IOErrors.
136     */
137    public DNSMessage query(Question q, String host, int port) throws IOException {
138        DNSMessage dnsMessage = (cache == null) ? null : cache.get(q);
139        if (dnsMessage != null) {
140            return dnsMessage;
141        }
142        DNSMessage message = new DNSMessage();
143        message.setQuestions(new Question[]{q});
144        message.setRecursionDesired(true);
145        message.setId(random.nextInt());
146        byte[] buf = message.toArray();
147        try (DatagramSocket socket = new DatagramSocket()) {
148            DatagramPacket packet = new DatagramPacket(buf, buf.length,
149                    InetAddress.getByName(host), port);
150            socket.setSoTimeout(timeout);
151            socket.send(packet);
152            packet = new DatagramPacket(new byte[bufferSize], bufferSize);
153            socket.receive(packet);
154            dnsMessage = DNSMessage.parse(packet.getData());
155            if (dnsMessage.getId() != message.getId()) {
156                return null;
157            }
158            for (Record record : dnsMessage.getAnswers()) {
159                if (record.isAnswer(q)) {
160                    if (cache != null) {
161                        cache.put(q, dnsMessage);
162                    }
163                    break;
164                }
165            }
166            return dnsMessage;
167        }
168    }
169
170    /**
171     * Query the system DNS server for one entry.
172     * @param q The question section of the DNS query.
173     * @return The response (or null on timeout/error).
174     */
175    public DNSMessage query(Question q) {
176        // While this query method does in fact re-use query(Question, String)
177        // we still do a cache lookup here in order to avoid unnecessary
178        // findDNS()calls, which are expensive on Android. Note that we do not
179        // put the results back into the Cache, as this is already done by
180        // query(Question, String).
181        DNSMessage message = cache.get(q);
182        if (message != null) {
183            return message;
184        }
185        String dnsServer[] = findDNS();
186        for (String dns : dnsServer) {
187            try {
188                message = query(q, dns);
189                if (message == null) {
190                    continue;
191                }
192                if (message.getResponseCode() !=
193                    DNSMessage.RESPONSE_CODE.NO_ERROR) {
194                    continue;
195                }
196                for (Record record: message.getAnswers()) {
197                    if (record.isAnswer(q)) {
198                        return message;
199                    }
200                }
201            } catch (IOException ioe) {
202                LOGGER.log(Level.FINE, "IOException in query", ioe);
203            }
204        }
205        return null;
206    }
207
208    /**
209     * Retrieve a list of currently configured DNS servers.
210     * @return The server array.
211     */
212    public String[] findDNS() {
213        String[] result = findDNSByReflection();
214        if (result != null) {
215            LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result));
216            return result;
217        }
218
219        result = findDNSByExec();
220        if (result != null) {
221            LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result));
222            return result;
223        }
224
225        // fallback for ipv4 and ipv6 connectivity
226        // see https://developers.google.com/speed/public-dns/docs/using
227        LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]");
228
229        return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"};
230    }
231
232    /**
233     * Try to retrieve the list of dns server by executing getprop.
234     * @return Array of servers, or null on failure.
235     */
236    protected String[] findDNSByExec() {
237        try {
238            Process process = Runtime.getRuntime().exec("getprop");
239            InputStream inputStream = process.getInputStream();
240            LineNumberReader lnr = new LineNumberReader(
241                new InputStreamReader(inputStream));
242            String line = null;
243            HashSet<String> server = new HashSet<String>(6);
244            while ((line = lnr.readLine()) != null) {
245                int split = line.indexOf("]: [");
246                if (split == -1) {
247                    continue;
248                }
249                String property = line.substring(1, split);
250                String value = line.substring(split + 4, line.length() - 1);
251                if (property.endsWith(".dns") || property.endsWith(".dns1") ||
252                    property.endsWith(".dns2") || property.endsWith(".dns3") ||
253                    property.endsWith(".dns4")) {
254
255                    // normalize the address
256
257                    InetAddress ip = InetAddress.getByName(value);
258
259                    if (ip == null) continue;
260
261                    value = ip.getHostAddress();
262
263                    if (value == null) continue;
264                    if (value.length() == 0) continue;
265
266                    server.add(value);
267                }
268            }
269            if (server.size() > 0) {
270                return server.toArray(new String[server.size()]);
271            }
272        } catch (IOException e) {
273            LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
274        }
275        return null;
276    }
277
278    /**
279     * Try to retrieve the list of dns server by calling SystemProperties.
280     * @return Array of servers, or null on failure.
281     */
282    protected String[] findDNSByReflection() {
283        try {
284            Class<?> SystemProperties =
285                    Class.forName("android.os.SystemProperties");
286            Method method = SystemProperties.getMethod("get",
287                    new Class[] { String.class });
288
289            ArrayList<String> servers = new ArrayList<String>(5);
290
291            for (String propKey : new String[] {
292                    "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
293
294                String value = (String)method.invoke(null, propKey);
295
296                if (value == null) continue;
297                if (value.length() == 0) continue;
298                if (servers.contains(value)) continue;
299
300                InetAddress ip = InetAddress.getByName(value);
301
302                if (ip == null) continue;
303
304                value = ip.getHostAddress();
305
306                if (value == null) continue;
307                if (value.length() == 0) continue;
308                if (servers.contains(value)) continue;
309
310                servers.add(value);
311            }
312
313            if (servers.size() > 0) {
314                return servers.toArray(new String[servers.size()]);
315            }
316        } catch (Exception e) {
317            // we might trigger some problems this way
318            LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
319        }
320        return null;
321    }
322
323}