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}