xmpp_client/src/main/java/de/measite/minidns/Client.java
Rene Treffer 4da60e7e20 Simplify cache and extend cache operations.
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 :-)
2014-06-12 22:39:51 +02:00

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;
}
}