
Remove the external cache dependency and use a simple LRU based on LinkedHashMap. Make it possible to get the parse time of DNSMessage, which means we can evaluate the TTL later on :-)
378 lines
12 KiB
Java
378 lines
12 KiB
Java
package de.measite.minidns;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.LineNumberReader;
|
|
import java.lang.reflect.Method;
|
|
import java.net.DatagramPacket;
|
|
import java.net.DatagramSocket;
|
|
import java.net.InetAddress;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.SecureRandom;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map.Entry;
|
|
import java.util.Random;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import de.measite.minidns.Record.CLASS;
|
|
import de.measite.minidns.Record.TYPE;
|
|
|
|
/**
|
|
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
|
|
* This circumvents the missing javax.naming package on android.
|
|
*/
|
|
public class Client {
|
|
|
|
private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
|
|
|
|
/**
|
|
* The internal random class for sequence generation.
|
|
*/
|
|
protected Random random;
|
|
|
|
/**
|
|
* The buffer size for dns replies.
|
|
*/
|
|
protected int bufferSize = 1500;
|
|
|
|
/**
|
|
* DNS timeout.
|
|
*/
|
|
protected int timeout = 5000;
|
|
|
|
/**
|
|
* The internal DNS cache.
|
|
*/
|
|
protected LinkedHashMap<Question, DNSMessage> cache;
|
|
|
|
/**
|
|
* Maximum acceptable ttl.
|
|
*/
|
|
protected long maxTTL = 60 * 60 * 1000;
|
|
|
|
/**
|
|
* Create a new DNS client.
|
|
*/
|
|
public Client() {
|
|
try {
|
|
random = SecureRandom.getInstance("SHA1PRNG");
|
|
} catch (NoSuchAlgorithmException e1) {
|
|
random = new SecureRandom();
|
|
}
|
|
setCacheSize(10);
|
|
}
|
|
|
|
/**
|
|
* Query a nameserver for a single entry.
|
|
* @param name The DNS name to request.
|
|
* @param type The DNS type to request (SRV, A, AAAA, ...).
|
|
* @param clazz The class of the request (usually IN for Internet).
|
|
* @param host The DNS server host.
|
|
* @param port The DNS server port.
|
|
* @return
|
|
* @throws IOException On IO Errors.
|
|
*/
|
|
public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port)
|
|
throws IOException
|
|
{
|
|
Question q = new Question(name, type, clazz);
|
|
return query(q, host, port);
|
|
}
|
|
|
|
/**
|
|
* Query a nameserver for a single entry.
|
|
* @param name The DNS name to request.
|
|
* @param type The DNS type to request (SRV, A, AAAA, ...).
|
|
* @param clazz The class of the request (usually IN for Internet).
|
|
* @param host The DNS server host.
|
|
* @return
|
|
* @throws IOException On IO Errors.
|
|
*/
|
|
public DNSMessage query(String name, TYPE type, CLASS clazz, String host)
|
|
throws IOException
|
|
{
|
|
Question q = new Question(name, type, clazz);
|
|
return query(q, host);
|
|
}
|
|
|
|
/**
|
|
* Query the system nameserver for a single entry.
|
|
* @param name The DNS name to request.
|
|
* @param type The DNS type to request (SRV, A, AAAA, ...).
|
|
* @param clazz The class of the request (usually IN for Internet).
|
|
* @return The DNSMessage reply or null.
|
|
*/
|
|
public DNSMessage query(String name, TYPE type, CLASS clazz)
|
|
{
|
|
Question q = new Question(name, type, clazz);
|
|
return query(q);
|
|
}
|
|
|
|
/**
|
|
* Query a specific server for one entry.
|
|
* @param q The question section of the DNS query.
|
|
* @param host The dns server host.
|
|
* @throws IOException On IOErrors.
|
|
*/
|
|
public DNSMessage query(Question q, String host) throws IOException {
|
|
return query(q, host, 53);
|
|
}
|
|
|
|
/**
|
|
* Query a specific server for one entry.
|
|
* @param q The question section of the DNS query.
|
|
* @param host The dns server host.
|
|
* @param port the dns port.
|
|
* @throws IOException On IOErrors.
|
|
*/
|
|
public DNSMessage query(Question q, String host, int port) throws IOException {
|
|
DNSMessage dnsMessage = (cache == null) ? null : cache.get(q);
|
|
if (dnsMessage != null && dnsMessage.getReceiveTimestamp() > 0l) {
|
|
// check the ttl
|
|
long ttl = maxTTL;
|
|
for (Record r : dnsMessage.getAnswers()) {
|
|
ttl = Math.min(ttl, r.ttl);
|
|
}
|
|
for (Record r : dnsMessage.getAdditionalResourceRecords()) {
|
|
ttl = Math.min(ttl, r.ttl);
|
|
}
|
|
if (dnsMessage.getReceiveTimestamp() + ttl <
|
|
System.currentTimeMillis()) {
|
|
return dnsMessage;
|
|
}
|
|
}
|
|
DNSMessage message = new DNSMessage();
|
|
message.setQuestions(new Question[]{q});
|
|
message.setRecursionDesired(true);
|
|
message.setId(random.nextInt());
|
|
byte[] buf = message.toArray();
|
|
try (DatagramSocket socket = new DatagramSocket()) {
|
|
DatagramPacket packet = new DatagramPacket(buf, buf.length,
|
|
InetAddress.getByName(host), port);
|
|
socket.setSoTimeout(timeout);
|
|
socket.send(packet);
|
|
packet = new DatagramPacket(new byte[bufferSize], bufferSize);
|
|
socket.receive(packet);
|
|
dnsMessage = DNSMessage.parse(packet.getData());
|
|
if (dnsMessage.getId() != message.getId()) {
|
|
return null;
|
|
}
|
|
for (Record record : dnsMessage.getAnswers()) {
|
|
if (record.isAnswer(q)) {
|
|
if (cache != null) {
|
|
cache.put(q, dnsMessage);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return dnsMessage;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query the system DNS server for one entry.
|
|
* @param q The question section of the DNS query.
|
|
*/
|
|
public DNSMessage query(Question q) {
|
|
// While this query method does in fact re-use query(Question, String)
|
|
// we still do a cache lookup here in order to avoid unnecessary
|
|
// findDNS()calls, which are expensive on Android. Note that we do not
|
|
// put the results back into the Cache, as this is already done by
|
|
// query(Question, String).
|
|
DNSMessage message = cache.get(q);
|
|
if (message != null) {
|
|
return message;
|
|
}
|
|
String dnsServer[] = findDNS();
|
|
for (String dns : dnsServer) {
|
|
try {
|
|
message = query(q, dns);
|
|
if (message == null) {
|
|
continue;
|
|
}
|
|
if (message.getResponseCode() !=
|
|
DNSMessage.RESPONSE_CODE.NO_ERROR) {
|
|
continue;
|
|
}
|
|
for (Record record: message.getAnswers()) {
|
|
if (record.isAnswer(q)) {
|
|
return message;
|
|
}
|
|
}
|
|
} catch (IOException ioe) {
|
|
LOGGER.log(Level.FINE, "IOException in query", ioe);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a list of currently configured DNS servers.
|
|
* @return The server array.
|
|
*/
|
|
public String[] findDNS() {
|
|
String[] result = findDNSByReflection();
|
|
if (result != null) {
|
|
LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result));
|
|
return result;
|
|
}
|
|
|
|
result = findDNSByExec();
|
|
if (result != null) {
|
|
LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result));
|
|
return result;
|
|
}
|
|
|
|
// fallback for ipv4 and ipv6 connectivity
|
|
// see https://developers.google.com/speed/public-dns/docs/using
|
|
LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]");
|
|
|
|
return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"};
|
|
}
|
|
|
|
/**
|
|
* Try to retrieve the list of dns server by executing getprop.
|
|
* @return Array of servers, or null on failure.
|
|
*/
|
|
protected String[] findDNSByExec() {
|
|
try {
|
|
Process process = Runtime.getRuntime().exec("getprop");
|
|
InputStream inputStream = process.getInputStream();
|
|
LineNumberReader lnr = new LineNumberReader(
|
|
new InputStreamReader(inputStream));
|
|
String line = null;
|
|
HashSet<String> server = new HashSet<String>(6);
|
|
while ((line = lnr.readLine()) != null) {
|
|
int split = line.indexOf("]: [");
|
|
if (split == -1) {
|
|
continue;
|
|
}
|
|
String property = line.substring(1, split);
|
|
String value = line.substring(split + 4, line.length() - 1);
|
|
if (property.endsWith(".dns") || property.endsWith(".dns1") ||
|
|
property.endsWith(".dns2") || property.endsWith(".dns3") ||
|
|
property.endsWith(".dns4")) {
|
|
|
|
// normalize the address
|
|
|
|
InetAddress ip = InetAddress.getByName(value);
|
|
|
|
if (ip == null) continue;
|
|
|
|
value = ip.getHostAddress();
|
|
|
|
if (value == null) continue;
|
|
if (value.length() == 0) continue;
|
|
|
|
server.add(value);
|
|
}
|
|
}
|
|
if (server.size() > 0) {
|
|
return server.toArray(new String[server.size()]);
|
|
}
|
|
} catch (IOException e) {
|
|
LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Try to retrieve the list of dns server by calling SystemProperties.
|
|
* @return Array of servers, or null on failure.
|
|
*/
|
|
protected String[] findDNSByReflection() {
|
|
try {
|
|
Class<?> SystemProperties =
|
|
Class.forName("android.os.SystemProperties");
|
|
Method method = SystemProperties.getMethod("get",
|
|
new Class[] { String.class });
|
|
|
|
ArrayList<String> servers = new ArrayList<String>(5);
|
|
|
|
for (String propKey : new String[] {
|
|
"net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
|
|
|
|
String value = (String)method.invoke(null, propKey);
|
|
|
|
if (value == null) continue;
|
|
if (value.length() == 0) continue;
|
|
if (servers.contains(value)) continue;
|
|
|
|
InetAddress ip = InetAddress.getByName(value);
|
|
|
|
if (ip == null) continue;
|
|
|
|
value = ip.getHostAddress();
|
|
|
|
if (value == null) continue;
|
|
if (value.length() == 0) continue;
|
|
if (servers.contains(value)) continue;
|
|
|
|
servers.add(value);
|
|
}
|
|
|
|
if (servers.size() > 0) {
|
|
return servers.toArray(new String[servers.size()]);
|
|
}
|
|
} catch (Exception e) {
|
|
// we might trigger some problems this way
|
|
LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Configure the cache size (default 10).
|
|
* @param maximumSize The new cache size or 0 to disable.
|
|
*/
|
|
@SuppressWarnings("serial")
|
|
public void setCacheSize(final int maximumSize) {
|
|
if (maximumSize == 0) {
|
|
this.cache = null;
|
|
} else {
|
|
LinkedHashMap<Question,DNSMessage> old = cache;
|
|
cache = new LinkedHashMap<Question,DNSMessage>() {
|
|
@Override
|
|
protected boolean removeEldestEntry(
|
|
Entry<Question, DNSMessage> eldest) {
|
|
return size() > maximumSize;
|
|
}
|
|
};
|
|
if (old != null) {
|
|
cache.putAll(old);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flush the DNS cache.
|
|
*/
|
|
public void flushCache() {
|
|
if (cache != null) {
|
|
cache.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current maximum record ttl.
|
|
* @return The maximum record ttl.
|
|
*/
|
|
public long getMaxTTL() {
|
|
return maxTTL;
|
|
}
|
|
|
|
/**
|
|
* Set the maximum record ttl.
|
|
* @param maxTTL The new maximum ttl.
|
|
*/
|
|
public void setMaxTTL(long maxTTL) {
|
|
this.maxTTL = maxTTL;
|
|
}
|
|
|
|
}
|