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