aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/measite/minidns/Client.java290
-rw-r--r--src/main/java/de/measite/minidns/DNSMessage.java507
-rw-r--r--src/main/java/de/measite/minidns/Question.java62
-rw-r--r--src/main/java/de/measite/minidns/Record.java297
-rw-r--r--src/main/java/de/measite/minidns/record/A.java37
-rw-r--r--src/main/java/de/measite/minidns/record/AAAA.java43
-rw-r--r--src/main/java/de/measite/minidns/record/CNAME.java44
-rw-r--r--src/main/java/de/measite/minidns/record/Data.java16
-rw-r--r--src/main/java/de/measite/minidns/record/NS.java12
-rw-r--r--src/main/java/de/measite/minidns/record/SRV.java74
-rw-r--r--src/main/java/de/measite/minidns/util/NameUtil.java91
11 files changed, 1473 insertions, 0 deletions
diff --git a/src/main/java/de/measite/minidns/Client.java b/src/main/java/de/measite/minidns/Client.java
new file mode 100644
index 00000000..fb42cd25
--- /dev/null
+++ b/src/main/java/de/measite/minidns/Client.java
@@ -0,0 +1,290 @@
+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.Random;
+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;
+
+ /**
+ * Create a new DNS client.
+ */
+ public Client() {
+ try {
+ random = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e1) {
+ random = new SecureRandom();
+ }
+ }
+
+ /**
+ * 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();
+ q.setClazz(clazz);
+ q.setType(type);
+ q.setName(name);
+ 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();
+ q.setClazz(clazz);
+ q.setType(type);
+ q.setName(name);
+ 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();
+ q.setClazz(clazz);
+ q.setType(type);
+ q.setName(name);
+ 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 message = new DNSMessage();
+ message.setQuestions(new Question[]{q});
+ message.setRecursionDesired(true);
+ message.setId(random.nextInt());
+ byte[] buf = message.toArray();
+ 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 = DNSMessage.parse(packet.getData());
+ if (dnsMessage.getId() != message.getId()) {
+ return null;
+ }
+ return dnsMessage;
+ }
+
+ /**
+ * Query the system DNS server for one entry.
+ * @param q The question section of the DNS query.
+ */
+ public DNSMessage query(Question q) {
+ String dnsServer[] = findDNS();
+ for (String dns : dnsServer) {
+ try {
+ DNSMessage 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) {
+ }
+ }
+ 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) {
+ e.printStackTrace();
+ }
+ 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
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/DNSMessage.java b/src/main/java/de/measite/minidns/DNSMessage.java
new file mode 100644
index 00000000..14c8f04b
--- /dev/null
+++ b/src/main/java/de/measite/minidns/DNSMessage.java
@@ -0,0 +1,507 @@
+package de.measite.minidns;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * A DNS message as defined by rfc1035. The message consists of a header and
+ * 4 sections: question, answer, nameserver and addition resource record
+ * section.
+ * A message can either be parsed ({@see #parse(byte[])}) or serialized
+ * ({@see #toArray()}).
+ */
+public class DNSMessage {
+
+ /**
+ * Possible DNS reply codes.
+ */
+ public static enum RESPONSE_CODE {
+ NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3),
+ NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7),
+ NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10);
+
+ /**
+ * Reverse lookup table for response codes.
+ */
+ private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{
+ NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP,
+ REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE,
+ null, null, null, null, null
+ };
+
+ /**
+ * The response code value.
+ */
+ private final byte value;
+
+ /**
+ * Create a new response code.
+ * @param value The response code value.
+ */
+ private RESPONSE_CODE(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Retrieve the byte value of the response code.
+ * @return
+ */
+ public byte getValue() {
+ return (byte) value;
+ }
+
+ /**
+ * Retrieve the response code for a byte value.
+ * @param value The byte value.
+ * @return The symbolic response code or null.
+ * @throws IllegalArgumentException if the value is not in the range of
+ * 0..15.
+ */
+ public static RESPONSE_CODE getResponseCode(int value) {
+ if (value < 0 || value > 15) {
+ throw new IllegalArgumentException();
+ }
+ return INVERSE_LUT[value];
+ }
+
+ };
+
+ /**
+ * Symbolic DNS Opcode values.
+ */
+ public static enum OPCODE {
+ QUERY(0),
+ INVERSE_QUERY(1),
+ STATUS(2),
+ NOTIFY(4),
+ UPDATE(5);
+
+ /**
+ * Lookup table for for obcode reolution.
+ */
+ private final static OPCODE INVERSE_LUT[] = new OPCODE[]{
+ QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null,
+ null, null, null, null, null, null, null, null
+ };
+
+ /**
+ * The value of this opcode.
+ */
+ private final byte value;
+
+ /**
+ * Create a new opcode for a given byte value.
+ * @param value The byte value of the opcode.
+ */
+ private OPCODE(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Retrieve the byte value of this opcode.
+ * @return The byte value of this opcode.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+ /**
+ * Retrieve the symbolic name of an opcode byte.
+ * @param value The byte value of the opcode.
+ * @return The symbolic opcode or null.
+ * @throws IllegalArgumentException If the byte value is not in the
+ * range 0..15.
+ */
+ public static OPCODE getOpcode(int value) {
+ if (value < 0 || value > 15) {
+ throw new IllegalArgumentException();
+ }
+ return INVERSE_LUT[value];
+ }
+
+ };
+
+ /**
+ * The DNS message id.
+ */
+ protected int id;
+
+ /**
+ * The DNS message opcode.
+ */
+ protected OPCODE opcode;
+
+ /**
+ * The response code of this dns message.
+ */
+ protected RESPONSE_CODE responseCode;
+
+ /**
+ * True if this is a query.
+ */
+ protected boolean query;
+
+ /**
+ * True if this is a authorative response.
+ */
+ protected boolean authoritativeAnswer;
+
+ /**
+ * True on truncate, tcp should be used.
+ */
+ protected boolean truncated;
+
+ /**
+ * True if the server should recurse.
+ */
+ protected boolean recursionDesired;
+
+ /**
+ * True if recursion is possible.
+ */
+ protected boolean recursionAvailable;
+
+ /**
+ * True if the server regarded the response as authentic.
+ */
+ protected boolean authenticData;
+
+ /**
+ * True if the server should not check the replies.
+ */
+ protected boolean checkDisabled;
+
+ /**
+ * The question section content.
+ */
+ protected Question questions[];
+
+ /**
+ * The answers section content.
+ */
+ protected Record answers[];
+
+ /**
+ * The nameserver records.
+ */
+ protected Record nameserverRecords[];
+
+ /**
+ * Additional resousrce records.
+ */
+ protected Record additionalResourceRecords[];
+
+ /**
+ * Retrieve the current DNS message id.
+ * @return The current DNS message id.
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Set the current DNS message id.
+ * @param id The new DNS message id.
+ */
+ public void setId(int id) {
+ this.id = id & 0xffff;
+ }
+
+ /**
+ * Retrieve the query type (true or false;
+ * @return True if this DNS message is a query.
+ */
+ public boolean isQuery() {
+ return query;
+ }
+
+ /**
+ * Set the query status of this message.
+ * @param query The new query status.
+ */
+ public void setQuery(boolean query) {
+ this.query = query;
+ }
+
+ /**
+ * True if the DNS message is an authoritative answer.
+ * @return True if this an authoritative DNS message.
+ */
+ public boolean isAuthoritativeAnswer() {
+ return authoritativeAnswer;
+ }
+
+ /**
+ * Set the authoritative answer flag.
+ * @param authoritativeAnswer Tge new authoritative answer value.
+ */
+ public void setAuthoritativeAnswer(boolean authoritativeAnswer) {
+ this.authoritativeAnswer = authoritativeAnswer;
+ }
+
+ /**
+ * Retrieve the truncation status of this message. True means that the
+ * client should try a tcp lookup.
+ * @return True if this message was truncated.
+ */
+ public boolean isTruncated() {
+ return truncated;
+ }
+
+ /**
+ * Set the truncation bit on this DNS message.
+ * @param truncated The new truncated bit status.
+ */
+ public void setTruncated(boolean truncated) {
+ this.truncated = truncated;
+ }
+
+ /**
+ * Check if this message preferes recursion.
+ * @return True if recursion is desired.
+ */
+ public boolean isRecursionDesired() {
+ return recursionDesired;
+ }
+
+ /**
+ * Set the recursion desired flag on this message.
+ * @param recursionDesired The new recusrion setting.
+ */
+ public void setRecursionDesired(boolean recursionDesired) {
+ this.recursionDesired = recursionDesired;
+ }
+
+ /**
+ * Retrieve the recursion available flag of this DNS message.
+ * @return The recursion available flag of this message.
+ */
+ public boolean isRecursionAvailable() {
+ return recursionAvailable;
+ }
+
+ /**
+ * Set the recursion available flog from this DNS message.
+ * @param recursionAvailable The new recursion available status.
+ */
+ public void setRecursionAvailable(boolean recursionAvailable) {
+ this.recursionAvailable = recursionAvailable;
+ }
+
+ /**
+ * Retrieve the authentic data flag of this message.
+ * @return The authentic data flag.
+ */
+ public boolean isAuthenticData() {
+ return authenticData;
+ }
+
+ /**
+ * Set the authentic data flag on this DNS message.
+ * @param authenticData The new authentic data flag value.
+ */
+ public void setAuthenticData(boolean authenticData) {
+ this.authenticData = authenticData;
+ }
+
+ /**
+ * Check if checks are disabled.
+ * @return The status of the CheckDisabled flag.
+ */
+ public boolean isCheckDisabled() {
+ return checkDisabled;
+ }
+
+ /**
+ * Change the check status of this packet.
+ * @param checkDisabled
+ */
+ public void setCheckDisabled(boolean checkDisabled) {
+ this.checkDisabled = checkDisabled;
+ }
+
+ /**
+ * Generate a binary dns packet out of this message.
+ * @return byte[] the binary representation.
+ * @throws IOException Should never happen.
+ */
+ public byte[] toArray() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
+ DataOutputStream dos = new DataOutputStream(baos);
+ int header = 0;
+ if (query) {
+ header += 1 << 15;
+ }
+ if (opcode != null) {
+ header += opcode.getValue() << 11;
+ }
+ if (authoritativeAnswer) {
+ header += 1 << 10;
+ }
+ if (truncated) {
+ header += 1 << 9;
+ }
+ if (recursionDesired) {
+ header += 1 << 8;
+ }
+ if (recursionAvailable) {
+ header += 1 << 7;
+ }
+ if (authenticData) {
+ header += 1 << 5;
+ }
+ if (checkDisabled) {
+ header += 1 << 4;
+ }
+ if (responseCode != null) {
+ header += responseCode.getValue();
+ }
+ dos.writeShort((short)id);
+ dos.writeShort((short)header);
+ if (questions == null) {
+ dos.writeShort(0);
+ } else {
+ dos.writeShort((short)questions.length);
+ }
+ if (answers == null) {
+ dos.writeShort(0);
+ } else {
+ dos.writeShort((short)answers.length);
+ }
+ if (nameserverRecords == null) {
+ dos.writeShort(0);
+ } else {
+ dos.writeShort((short)nameserverRecords.length);
+ }
+ if (additionalResourceRecords == null) {
+ dos.writeShort(0);
+ } else {
+ dos.writeShort((short)additionalResourceRecords.length);
+ }
+ for (Question question: questions) {
+ dos.write(question.toByteArray());
+ }
+ dos.flush();
+ return baos.toByteArray();
+ }
+
+ /**
+ * Build a DNS Message based on a binary DNS message.
+ * @param data The DNS message data.
+ * @return Parsed DNSMessage message.
+ * @throws IOException On read errors.
+ */
+ public static DNSMessage parse(byte data[]) throws IOException {
+ ByteArrayInputStream bis = new ByteArrayInputStream(data);
+ DataInputStream dis = new DataInputStream(bis);
+ DNSMessage message = new DNSMessage();
+ message.id = dis.readUnsignedShort();
+ int header = dis.readUnsignedShort();
+ message.query = ((header >> 15) & 1) == 0;
+ message.opcode = OPCODE.getOpcode((header >> 11) & 0xf);
+ message.authoritativeAnswer = ((header >> 10) & 1) == 1;
+ message.truncated = ((header >> 9) & 1) == 1;
+ message.recursionDesired = ((header >> 8) & 1) == 1;
+ message.recursionAvailable = ((header >> 7) & 1) == 1;
+ message.authenticData = ((header >> 5) & 1) == 1;
+ message.checkDisabled = ((header >> 4) & 1) == 1;
+ message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf);
+ int questionCount = dis.readUnsignedShort();
+ int answerCount = dis.readUnsignedShort();
+ int nameserverCount = dis.readUnsignedShort();
+ int additionalResourceRecordCount = dis.readUnsignedShort();
+ message.questions = new Question[questionCount];
+ while (questionCount-- > 0) {
+ Question q = new Question();
+ q.parse(dis, data);
+ message.questions[questionCount] = q;
+ }
+ message.answers = new Record[answerCount];
+ while (answerCount-- > 0) {
+ Record rr = new Record();
+ rr.parse(dis, data);
+ message.answers[answerCount] = rr;
+ }
+ message.nameserverRecords = new Record[nameserverCount];
+ while (nameserverCount-- > 0) {
+ Record rr = new Record();
+ rr.parse(dis, data);
+ message.nameserverRecords[nameserverCount] = rr;
+ }
+ message.additionalResourceRecords =
+ new Record[additionalResourceRecordCount];
+ while (additionalResourceRecordCount-- > 0) {
+ Record rr = new Record();
+ rr.parse(dis, data);
+ message.additionalResourceRecords[additionalResourceRecordCount] =
+ rr;
+ }
+ return message;
+ }
+
+ /**
+ * Set the question part of this message.
+ * @param questions The questions.
+ */
+ public void setQuestions(Question ... questions) {
+ this.questions = questions;
+ }
+
+ /**
+ * Retrieve the opcode of this message.
+ * @return The opcode of this message.
+ */
+ public OPCODE getOpcode() {
+ return opcode;
+ }
+
+ /**
+ * Retrieve the response code of this message.
+ * @return The response code.
+ */
+ public RESPONSE_CODE getResponseCode() {
+ return responseCode;
+ }
+
+ /**
+ * Retrieve the question section of this message.
+ * @return The DNS question section.
+ */
+ public Question[] getQuestions() {
+ return questions;
+ }
+
+ /**
+ * Retrieve the answer records of this DNS message.
+ * @return The answer section of this DNS message.
+ */
+ public Record[] getAnswers() {
+ return answers;
+ }
+
+ /**
+ * Retrieve the nameserver records of this DNS message.
+ * @return The nameserver section of this DNS message.
+ */
+ public Record[] getNameserverRecords() {
+ return nameserverRecords;
+ }
+
+ /**
+ * Retrieve the additional resource records attached to this DNS message.
+ * @return The additional resource record section of this DNS message.
+ */
+ public Record[] getAdditionalResourceRecords() {
+ return additionalResourceRecords;
+ }
+
+ public String toString() {
+ return "-- DNSMessage " + id + " --\n" +
+ Arrays.toString(answers);
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/Question.java b/src/main/java/de/measite/minidns/Question.java
new file mode 100644
index 00000000..9d1e3f56
--- /dev/null
+++ b/src/main/java/de/measite/minidns/Question.java
@@ -0,0 +1,62 @@
+package de.measite.minidns;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.util.NameUtil;
+
+public class Question {
+
+ private String name;
+
+ private TYPE type;
+
+ private CLASS clazz = CLASS.IN;
+
+ public TYPE getType() {
+ return type;
+ }
+
+ public void setType(TYPE type) {
+ this.type = type;
+ }
+
+ public CLASS getClazz() {
+ return clazz;
+ }
+
+ public void setClazz(CLASS clazz) {
+ this.clazz = clazz;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void parse(DataInputStream dis, byte[] data) throws IOException {
+ this.name = NameUtil.parse(dis, data);
+ this.type = TYPE.getType(dis.readUnsignedShort());
+ this.clazz = CLASS.getClass(dis.readUnsignedShort());
+ }
+
+ public byte[] toByteArray() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ dos.write(NameUtil.toByteArray(this.name));
+ dos.writeShort(type.getValue());
+ dos.writeShort(clazz.getValue());
+
+ dos.flush();
+ return baos.toByteArray();
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/Record.java b/src/main/java/de/measite/minidns/Record.java
new file mode 100644
index 00000000..fb0b5d5f
--- /dev/null
+++ b/src/main/java/de/measite/minidns/Record.java
@@ -0,0 +1,297 @@
+package de.measite.minidns;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.CNAME;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.record.NS;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.util.NameUtil;
+
+/**
+ * A generic DNS record.
+ */
+public class Record {
+
+ /**
+ * The record type.
+ * {@see http://www.iana.org/assignments/dns-parameters}
+ */
+ public static enum TYPE {
+ A(1),
+ NS(2),
+ MD(3),
+ MF(4),
+ CNAME(5),
+ SOA(6),
+ MB(7),
+ MG(8),
+ MR(9),
+ NULL(10),
+ WKS(11),
+ PTR(12),
+ HINFO(13),
+ MINFO(14),
+ MX(15),
+ TXT(16),
+ RP(17),
+ AFSDB(18),
+ X25(19),
+ ISDN(20),
+ RT(21),
+ NSAP(22),
+ NSAP_PTR(23),
+ SIG(24),
+ KEY(25),
+ PX(26),
+ GPOS(27),
+ AAAA(28),
+ LOC(29),
+ NXT(30),
+ EID(31),
+ NIMLOC(32),
+ SRV(33),
+ ATMA(34),
+ NAPTR(35),
+ KX(36),
+ CERT(37),
+ A6(38),
+ DNAME(39),
+ SINK(40),
+ OPT(41),
+ APL(42),
+ DS(43),
+ SSHFP(44),
+ IPSECKEY(45),
+ RRSIG(46),
+ NSEC(47),
+ DNSKEY(48),
+ DHCID(49),
+ NSEC3(50),
+ NSEC3PARAM(51),
+ HIP(55),
+ NINFO(56),
+ RKEY(57),
+ TALINK(58),
+ SPF(99),
+ UINFO(100),
+ UID(101),
+ GID(102),
+ TKEY(249),
+ TSIG(250),
+ IXFR(251),
+ AXFR(252),
+ MAILB(253),
+ MAILA(254),
+ ANY(255),
+ TA(32768),
+ DLV(32769);
+
+ /**
+ * The value of this DNS record type.
+ */
+ private final int value;
+
+ /**
+ * Internal lookup table to map values to types.
+ */
+ private final static HashMap<Integer, TYPE> INVERSE_LUT =
+ new HashMap<Integer, TYPE>();
+
+ /**
+ * Initialize the reverse lookup table.
+ */
+ static {
+ for(TYPE t: TYPE.values()) {
+ INVERSE_LUT.put(t.getValue(), t);
+ }
+ }
+
+ /**
+ * Create a new record type.
+ * @param value The binary value of this type.
+ */
+ private TYPE(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Retrieve the binary value of this type.
+ * @return The binary value.
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Retrieve the symbolic type of the binary value.
+ * @param value The binary type value.
+ * @return The symbolic tpye.
+ */
+ public static TYPE getType(int value) {
+ return INVERSE_LUT.get(value);
+ }
+ };
+
+ /**
+ * The symbolic class of a DNS record (usually IN for Internet).
+ */
+ public static enum CLASS {
+ IN(1),
+ CH(3),
+ HS(4),
+ NONE(254),
+ ANY(255);
+
+ /**
+ * Internal reverse lookup table to map binary class values to symbolic
+ * names.
+ */
+ private final static HashMap<Integer, CLASS> INVERSE_LUT =
+ new HashMap<Integer, CLASS>();
+
+ /**
+ * Initialize the interal reverse lookup table.
+ */
+ static {
+ for(CLASS c: CLASS.values()) {
+ INVERSE_LUT.put(c.getValue(), c);
+ }
+ }
+
+ /**
+ * The binary value of this dns class.
+ */
+ private final int value;
+
+ /**
+ * Create a new DNS class based on a binary value.
+ * @param value The binary value of this DNS class.
+ */
+ private CLASS(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Retrieve the binary value of this DNS class.
+ * @return The binary value of this DNS class.
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Retrieve the symbolic DNS class for a binary class value.
+ * @param value The binary DNS class value.
+ * @return The symbolic class instance.
+ */
+ public static CLASS getClass(int value) {
+ return INVERSE_LUT.get(value);
+ }
+
+ }
+
+ /**
+ * The generic name of this record.
+ */
+ protected String name;
+
+ /**
+ * The type (and payload type) of this record.
+ */
+ protected TYPE type;
+
+ /**
+ * The record class (usually CLASS.IN).
+ */
+ protected CLASS clazz;
+
+ /**
+ * The ttl of this record.
+ */
+ protected long ttl;
+
+ /**
+ * The payload object of this record.
+ */
+ protected Data payloadData;
+
+ /**
+ * Parse a given record based on the full message data and the current
+ * stream position.
+ * @param dis The DataInputStream positioned at the first record byte.
+ * @param data The full message data.
+ * @throws IOException In case of malformed replies.
+ */
+ public void parse(DataInputStream dis, byte[] data) throws IOException {
+ this.name = NameUtil.parse(dis, data);
+ this.type = TYPE.getType(dis.readUnsignedShort());
+ this.clazz = CLASS.getClass(dis.readUnsignedShort());
+ this.ttl = (((long)dis.readUnsignedShort()) << 32) +
+ dis.readUnsignedShort();
+ int payloadLength = dis.readUnsignedShort();
+ switch (this.type) {
+ case SRV:
+ this.payloadData = new SRV();
+ break;
+ case AAAA:
+ this.payloadData = new AAAA();
+ break;
+ case A:
+ this.payloadData = new A();
+ break;
+ case NS:
+ this.payloadData = new NS();
+ break;
+ case CNAME:
+ this.payloadData = new CNAME();
+ break;
+ default:
+ System.out.println("Unparsed type " + type);
+ this.payloadData = null;
+ for (int i = 0; i < payloadLength; i++) {
+ dis.readByte();
+ }
+ break;
+ }
+ if (this.payloadData != null) {
+ this.payloadData.parse(dis, data, payloadLength);
+ }
+ }
+
+ /**
+ * Retrieve a textual representation of this resource record.
+ * @return String
+ */
+ @Override
+ public String toString() {
+ if (payloadData == null) {
+ return "RR " + type + "/" + clazz;
+ }
+ return "RR " + type + "/" + clazz + ": " + payloadData.toString();
+ };
+
+ /**
+ * Check if this record answers a given query.
+ * @param q The query.
+ * @return True if this record is a valid answer.
+ */
+ public boolean isAnswer(Question q) {
+ return ((q.getType() == type) || (q.getType() == TYPE.ANY)) &&
+ ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) &&
+ (q.getName().equals(name));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Data getPayload() {
+ return payloadData;
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/record/A.java b/src/main/java/de/measite/minidns/record/A.java
new file mode 100644
index 00000000..a85a7af0
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/A.java
@@ -0,0 +1,37 @@
+package de.measite.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.TYPE;
+
+public class A implements Data {
+
+ private byte[] ip;
+
+ @Override
+ public TYPE getType() {
+ return TYPE.A;
+ }
+
+ @Override
+ public byte[] toByteArray() {
+ return ip;
+ }
+
+ @Override
+ public void parse(DataInputStream dis, byte[] data, int length)
+ throws IOException {
+ ip = new byte[4];
+ dis.readFully(ip);
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(ip[0] & 0xff) + "." +
+ Integer.toString(ip[1] & 0xff) + "." +
+ Integer.toString(ip[2] & 0xff) + "." +
+ Integer.toString(ip[3] & 0xff);
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/record/AAAA.java b/src/main/java/de/measite/minidns/record/AAAA.java
new file mode 100644
index 00000000..d89147b2
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/AAAA.java
@@ -0,0 +1,43 @@
+package de.measite.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.TYPE;
+
+public class AAAA implements Data {
+
+ private byte[] ip;
+
+ @Override
+ public TYPE getType() {
+ return TYPE.AAAA;
+ }
+
+ @Override
+ public byte[] toByteArray() {
+ return ip;
+ }
+
+ @Override
+ public void parse(DataInputStream dis, byte[] data, int length)
+ throws IOException {
+ ip = new byte[16];
+ dis.readFully(ip);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < ip.length; i += 2) {
+ if (i != 0) {
+ sb.append(':');
+ }
+ sb.append(Integer.toHexString(
+ ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff)
+ ));
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/record/CNAME.java b/src/main/java/de/measite/minidns/record/CNAME.java
new file mode 100644
index 00000000..4657b4a5
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/CNAME.java
@@ -0,0 +1,44 @@
+package de.measite.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.util.NameUtil;
+
+public class CNAME implements Data {
+
+ protected String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public byte[] toByteArray() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void parse(DataInputStream dis, byte[] data, int length)
+ throws IOException
+ {
+ this.name = NameUtil.parse(dis, data);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.CNAME;
+ }
+
+ @Override
+ public String toString() {
+ return "to \"" + name + "\"";
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/record/Data.java b/src/main/java/de/measite/minidns/record/Data.java
new file mode 100644
index 00000000..9cb80374
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/Data.java
@@ -0,0 +1,16 @@
+package de.measite.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.TYPE;
+
+public interface Data {
+
+ TYPE getType();
+
+ byte[] toByteArray();
+
+ void parse(DataInputStream dis, byte data[], int length) throws IOException;
+
+}
diff --git a/src/main/java/de/measite/minidns/record/NS.java b/src/main/java/de/measite/minidns/record/NS.java
new file mode 100644
index 00000000..bf07e8c4
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/NS.java
@@ -0,0 +1,12 @@
+package de.measite.minidns.record;
+
+import de.measite.minidns.Record.TYPE;
+
+public class NS extends CNAME {
+
+ @Override
+ public TYPE getType() {
+ return TYPE.NS;
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/record/SRV.java b/src/main/java/de/measite/minidns/record/SRV.java
new file mode 100644
index 00000000..32b70c4d
--- /dev/null
+++ b/src/main/java/de/measite/minidns/record/SRV.java
@@ -0,0 +1,74 @@
+package de.measite.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.util.NameUtil;
+
+public class SRV implements Data {
+
+ protected int priority;
+ protected int weight;
+ protected int port;
+ protected String name;
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public void setPriority(int priority) {
+ this.priority = priority;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = weight;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public byte[] toByteArray() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void parse(DataInputStream dis, byte[] data, int length)
+ throws IOException
+ {
+ this.priority = dis.readUnsignedShort();
+ this.weight = dis.readUnsignedShort();
+ this.port = dis.readUnsignedShort();
+ this.name = NameUtil.parse(dis, data);
+ }
+
+ @Override
+ public String toString() {
+ return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight;
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.SRV;
+ }
+
+}
diff --git a/src/main/java/de/measite/minidns/util/NameUtil.java b/src/main/java/de/measite/minidns/util/NameUtil.java
new file mode 100644
index 00000000..91a6649d
--- /dev/null
+++ b/src/main/java/de/measite/minidns/util/NameUtil.java
@@ -0,0 +1,91 @@
+package de.measite.minidns.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.IDN;
+import java.util.HashSet;
+import java.util.Arrays;
+
+public class NameUtil {
+
+ public static int size(String name) {
+ return name.length() + 2;
+ }
+
+ public static boolean idnEquals(String name1, String name2) {
+ if (name1 == name2) return true; // catches null, null
+ if (name1 == null) return false;
+ if (name2 == null) return false;
+ if (name1.equals(name2)) return true;
+
+ try {
+ return Arrays.equals(toByteArray(name1),toByteArray(name2));
+ } catch (IOException e) {
+ return false; // impossible
+ }
+ }
+
+ public static byte[] toByteArray(String name) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(64);
+ DataOutputStream dos = new DataOutputStream(baos);
+ for (String s: name.split("[.\u3002\uFF0E\uFF61]")) {
+ byte[] buffer = IDN.toASCII(s).getBytes();
+ dos.writeByte(buffer.length);
+ dos.write(buffer);
+ }
+ dos.writeByte(0);
+ dos.flush();
+ return baos.toByteArray();
+ }
+
+ public static String parse(DataInputStream dis, byte data[])
+ throws IOException
+ {
+ int c = dis.readUnsignedByte();
+ if ((c & 0xc0) == 0xc0) {
+ c = ((c & 0x3f) << 8) + dis.readUnsignedByte();
+ HashSet<Integer> jumps = new HashSet<Integer>();
+ jumps.add(c);
+ return parse(data, c, jumps);
+ }
+ if (c == 0) {
+ return "";
+ }
+ byte b[] = new byte[c];
+ dis.readFully(b);
+ String s = IDN.toUnicode(new String(b));
+ String t = parse(dis, data);
+ if (t.length() > 0) {
+ s = s + "." + t;
+ }
+ return s;
+ }
+
+ public static String parse(
+ byte data[],
+ int offset,
+ HashSet<Integer> jumps
+ ) {
+ int c = data[offset] & 0xff;
+ if ((c & 0xc0) == 0xc0) {
+ c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff);
+ if (jumps.contains(c)) {
+ throw new IllegalStateException("Cyclic offsets detected.");
+ }
+ jumps.add(c);
+ return parse(data, c, jumps);
+ }
+ if (c == 0) {
+ return "";
+ }
+ String s = new String(data,offset + 1, c);
+ String t = parse(data, offset + 1 + c, jumps);
+ if (t.length() > 0) {
+ s = s + "." + t;
+ }
+ return s;
+ }
+
+}