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}