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}