> getAdditionalResourceRecords() {
+ if (additionalSection == null) {
+ return Collections.emptyList();
+ }
+ return additionalSection;
+ }
+
+ /**
+ * Get the @{link EDNS} builder. If no builder has been set so far, then a new one will be created.
+ *
+ * The EDNS record can be used to announce the supported size of UDP payload as well as additional flags.
+ *
+ *
+ * Note that some networks and firewalls are known to block big UDP payloads. 1280 should be a reasonable value,
+ * everything below 512 is treated as 512 and should work on all networks.
+ *
+ *
+ * @return a EDNS builder.
+ */
+ public Edns.Builder getEdnsBuilder() {
+ if (ednsBuilder == null) {
+ ednsBuilder = Edns.builder();
+ }
+ return ednsBuilder;
+ }
+
+ public DnsMessage build() {
+ return new DnsMessage(this);
+ }
+
+ private void writeToStringBuilder(StringBuilder sb) {
+ sb.append('(')
+ .append(id)
+ .append(' ')
+ .append(opcode)
+ .append(' ')
+ .append(responseCode)
+ .append(' ');
+ if (query) {
+ sb.append("resp[qr=1]");
+ } else {
+ sb.append("query[qr=0]");
+ }
+ if (authoritativeAnswer)
+ sb.append(" aa");
+ if (truncated)
+ sb.append(" tr");
+ if (recursionDesired)
+ sb.append(" rd");
+ if (recursionAvailable)
+ sb.append(" ra");
+ if (authenticData)
+ sb.append(" ad");
+ if (checkingDisabled)
+ sb.append(" cd");
+ sb.append(")\n");
+ if (questions != null) {
+ for (Question question : questions) {
+ sb.append("[Q: ").append(question).append("]\n");
+ }
+ }
+ if (answerSection != null) {
+ for (Record extends Data> record : answerSection) {
+ sb.append("[A: ").append(record).append("]\n");
+ }
+ }
+ if (authoritySection != null) {
+ for (Record extends Data> record : authoritySection) {
+ sb.append("[N: ").append(record).append("]\n");
+ }
+ }
+ if (additionalSection != null) {
+ for (Record extends Data> record : additionalSection) {
+ sb.append("[X: ");
+ Edns edns = Edns.fromRecord(record);
+ if (edns != null) {
+ sb.append(edns.toString());
+ } else {
+ sb.append(record);
+ }
+ sb.append("]\n");
+ }
+ }
+
+ // Strip trailing newline.
+ if (sb.charAt(sb.length() - 1) == '\n') {
+ sb.setLength(sb.length() - 1);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Builder of DnsMessage");
+ writeToStringBuilder(sb);
+ return sb.toString();
+ }
+ }
+
+}
diff --git a/core/java/src/org/minidns/dnsmessage/Question.java b/core/java/src/org/minidns/dnsmessage/Question.java
new file mode 100644
index 000000000..c187622b0
--- /dev/null
+++ b/core/java/src/org/minidns/dnsmessage/Question.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.dnsmessage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.CLASS;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * A DNS question (request).
+ */
+public class Question {
+
+ /**
+ * The question string (e.g. "measite.de").
+ */
+ public final DnsName name;
+
+ /**
+ * The question type (e.g. A).
+ */
+ public final TYPE type;
+
+ /**
+ * The question class (usually IN for Internet).
+ */
+ public final CLASS clazz;
+
+ /**
+ * UnicastQueries have the highest bit of the CLASS field set to 1.
+ */
+ private final boolean unicastQuery;
+
+ /**
+ * Cache for the serialized object.
+ */
+ private byte[] byteArray;
+
+ /**
+ * Create a dns question for the given name/type/class.
+ * @param name The name e.g. "measite.de".
+ * @param type The type, e.g. A.
+ * @param clazz The class, usually IN (internet).
+ * @param unicastQuery True if this is a unicast query.
+ */
+ public Question(CharSequence name, TYPE type, CLASS clazz, boolean unicastQuery) {
+ this(DnsName.from(name), type, clazz, unicastQuery);
+ }
+
+ public Question(DnsName name, TYPE type, CLASS clazz, boolean unicastQuery) {
+ assert name != null;
+ assert type != null;
+ assert clazz != null;
+ this.name = name;
+ this.type = type;
+ this.clazz = clazz;
+ this.unicastQuery = unicastQuery;
+ }
+
+ /**
+ * Create a dns question for the given name/type/class.
+ * @param name The name e.g. "measite.de".
+ * @param type The type, e.g. A.
+ * @param clazz The class, usually IN (internet).
+ */
+ public Question(DnsName name, TYPE type, CLASS clazz) {
+ this(name, type, clazz, false);
+ }
+
+ /**
+ * Create a dns question for the given name/type/IN (internet class).
+ * @param name The name e.g. "measite.de".
+ * @param type The type, e.g. A.
+ */
+ public Question(DnsName name, TYPE type) {
+ this(name, type, CLASS.IN);
+ }
+
+ /**
+ * Create a dns question for the given name/type/class.
+ * @param name The name e.g. "measite.de".
+ * @param type The type, e.g. A.
+ * @param clazz The class, usually IN (internet).
+ */
+ public Question(CharSequence name, TYPE type, CLASS clazz) {
+ this(DnsName.from(name), type, clazz);
+ }
+
+ /**
+ * Create a dns question for the given name/type/IN (internet class).
+ * @param name The name e.g. "measite.de".
+ * @param type The type, e.g. A.
+ */
+ public Question(CharSequence name, TYPE type) {
+ this(DnsName.from(name), type);
+ }
+
+ /**
+ * Parse a byte array and rebuild the dns question from it.
+ * @param dis The input stream.
+ * @param data The plain data (for dns name references).
+ * @throws IOException On errors (read outside of packet).
+ */
+ public Question(DataInputStream dis, byte[] data) throws IOException {
+ name = DnsName.parse(dis, data);
+ type = TYPE.getType(dis.readUnsignedShort());
+ clazz = CLASS.getClass(dis.readUnsignedShort());
+ unicastQuery = false;
+ }
+
+ /**
+ * Generate a binary paket for this dns question.
+ * @return The dns question.
+ */
+ public byte[] toByteArray() {
+ if (byteArray == null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ try {
+ name.writeToStream(dos);
+ dos.writeShort(type.getValue());
+ dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0));
+ dos.flush();
+ } catch (IOException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ }
+ byteArray = baos.toByteArray();
+ }
+ return byteArray;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(toByteArray());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Question)) {
+ return false;
+ }
+ byte[] t = toByteArray();
+ byte[] o = ((Question) other).toByteArray();
+ return Arrays.equals(t, o);
+ }
+
+ @Override
+ public String toString() {
+ return name.getRawAce() + ".\t" + clazz + '\t' + type;
+ }
+
+ public DnsMessage.Builder asMessageBuilder() {
+ DnsMessage.Builder builder = DnsMessage.builder();
+ builder.setQuestion(this);
+ return builder;
+ }
+
+ public DnsMessage asQueryMessage() {
+ return asMessageBuilder().build();
+ }
+}
diff --git a/core/java/src/org/minidns/dnsname/DnsName.java b/core/java/src/org/minidns/dnsname/DnsName.java
new file mode 100644
index 000000000..46e9d1749
--- /dev/null
+++ b/core/java/src/org/minidns/dnsname/DnsName.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.dnsname;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+
+import org.minidns.dnslabel.DnsLabel;
+import org.minidns.idna.MiniDnsIdna;
+
+/**
+ * A DNS name, also called "domain name". A DNS name consists of multiple 'labels' and is subject to certain restrictions (see
+ * for example RFC 3696 § 2.).
+ *
+ * Instances of this class can be created by using {@link #from(String)}.
+ *
+ *
+ * This class holds three representations of a DNS name: ACE, raw ACE and IDN. ACE (ASCII Compatible Encoding), which
+ * can be accessed via {@link #ace}, represents mostly the data that got send over the wire. But since DNS names are
+ * case insensitive, the ACE value is normalized to lower case. You can use {@link #getRawAce()} to get the raw ACE data
+ * that was received, which possibly includes upper case characters. The IDN (Internationalized Domain Name), that is
+ * the DNS name as it should be shown to the user, can be retrieved using {@link #asIdn()}.
+ *
+ * More information about Internationalized Domain Names can be found at:
+ *
+ *
+ * @see RFC 3696
+ * @author Florian Schmaus
+ *
+ */
+public final class DnsName implements CharSequence, Serializable, Comparable {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @see RFC 3490 § 3.1 1.
+ */
+ private static final String LABEL_SEP_REGEX = "[.\u3002\uFF0E\uFF61]";
+
+ /**
+ * @see RFC 1035 § 2.3.4.reverse order.
+ */
+ private transient DnsLabel[] labels;
+
+ private transient DnsLabel[] rawLabels;
+
+ private transient int hashCode;
+
+ private int size = -1;
+
+ private DnsName(String name) {
+ this(name, true);
+ }
+
+ private DnsName(String name, boolean inAce) {
+ if (name.isEmpty()) {
+ rawAce = ROOT.rawAce;
+ } else {
+ final int nameLength = name.length();
+ final int nameLastPos = nameLength - 1;
+
+ // Strip potential trailing dot. N.B. that we require nameLength > 2, because we don't want to strip the one
+ // character string containing only a single dot to the empty string.
+ if (nameLength >= 2 && name.charAt(nameLastPos) == '.') {
+ name = name.subSequence(0, nameLastPos).toString();
+ }
+
+ if (inAce) {
+ // Name is already in ACE format.
+ rawAce = name;
+ } else {
+ rawAce = MiniDnsIdna.toASCII(name);
+ }
+ }
+
+ ace = rawAce.toLowerCase(Locale.US);
+
+ if (!VALIDATE) {
+ return;
+ }
+
+ // Validate the DNS name.
+ validateMaxDnsnameLengthInOctets();
+ }
+
+ private DnsName(DnsLabel[] rawLabels, boolean validateMaxDnsnameLength) {
+ this.rawLabels = rawLabels;
+ this.labels = new DnsLabel[rawLabels.length];
+
+ int size = 0;
+ for (int i = 0; i < rawLabels.length; i++) {
+ size += rawLabels[i].length() + 1;
+ labels[i] = rawLabels[i].asLowercaseVariant();
+ }
+
+ rawAce = labelsToString(rawLabels, size);
+ ace = labelsToString(labels, size);
+
+ // The following condition is deliberately designed that VALIDATE=false causes the validation to be skipped even
+ // if validateMaxDnsnameLength is set to true. There is no need to validate even if this constructor is called
+ // with validateMaxDnsnameLength set to true if VALIDATE is globally set to false.
+ if (!validateMaxDnsnameLength || !VALIDATE) {
+ return;
+ }
+
+ validateMaxDnsnameLengthInOctets();
+ }
+
+ private static String labelsToString(DnsLabel[] labels, int stringLength) {
+ StringBuilder sb = new StringBuilder(stringLength);
+ for (int i = labels.length - 1; i >= 0; i--) {
+ sb.append(labels[i]).append('.');
+ }
+ sb.setLength(sb.length() - 1);
+ return sb.toString();
+ }
+
+ private void validateMaxDnsnameLengthInOctets() {
+ setBytesIfRequired();
+ if (bytes.length > MAX_DNSNAME_LENGTH_IN_OCTETS) {
+ throw new InvalidDnsNameException.DNSNameTooLongException(ace, bytes);
+ }
+ }
+
+ public void writeToStream(OutputStream os) throws IOException {
+ setBytesIfRequired();
+ os.write(bytes);
+ }
+
+ /**
+ * Serialize a domain name under IDN rules.
+ *
+ * @return The binary domain name representation.
+ */
+ public byte[] getBytes() {
+ setBytesIfRequired();
+ return bytes.clone();
+ }
+
+ public byte[] getRawBytes() {
+ if (rawBytes == null) {
+ setLabelsIfRequired();
+ rawBytes = toBytes(rawLabels);
+ }
+
+ return rawBytes.clone();
+ }
+
+ private void setBytesIfRequired() {
+ if (bytes != null)
+ return;
+
+ setLabelsIfRequired();
+ bytes = toBytes(labels);
+ }
+
+ private static byte[] toBytes(DnsLabel[] labels) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(64);
+ for (int i = labels.length - 1; i >= 0; i--) {
+ labels[i].writeToBoas(baos);
+ }
+
+ baos.write(0);
+
+ assert baos.size() <= MAX_DNSNAME_LENGTH_IN_OCTETS;
+
+ return baos.toByteArray();
+ }
+
+ private void setLabelsIfRequired() {
+ if (labels != null && rawLabels != null) return;
+
+ if (isRootLabel()) {
+ rawLabels = labels = new DnsLabel[0];
+ return;
+ }
+
+ labels = getLabels(ace);
+ rawLabels = getLabels(rawAce);
+ }
+
+ private static DnsLabel[] getLabels(String ace) {
+ String[] labels = ace.split(LABEL_SEP_REGEX, MAX_LABELS);
+
+ // Reverse the labels, so that 'foo, example, org' becomes 'org, example, foo'.
+ for (int i = 0; i < labels.length / 2; i++) {
+ String t = labels[i];
+ int j = labels.length - i - 1;
+ labels[i] = labels[j];
+ labels[j] = t;
+ }
+
+ try {
+ return DnsLabel.from(labels);
+ } catch (DnsLabel.LabelToLongException e) {
+ throw new InvalidDnsNameException.LabelTooLongException(ace, e.label);
+ }
+ }
+
+ public String getRawAce() {
+ return rawAce;
+ }
+
+ public String asIdn() {
+ if (idn != null)
+ return idn;
+
+ idn = MiniDnsIdna.toUnicode(ace);
+ return idn;
+ }
+
+ /**
+ * Domainpart in ACE representation.
+ *
+ * @return the domainpart in ACE representation.
+ */
+ public String getDomainpart() {
+ setHostnameAndDomainpartIfRequired();
+ return domainpart;
+ }
+
+ /**
+ * Hostpart in ACE representation.
+ *
+ * @return the hostpart in ACE representation.
+ */
+ public String getHostpart() {
+ setHostnameAndDomainpartIfRequired();
+ return hostpart;
+ }
+
+ public DnsLabel getHostpartLabel() {
+ setLabelsIfRequired();
+ return labels[labels.length];
+ }
+
+ private void setHostnameAndDomainpartIfRequired() {
+ if (hostpart != null) return;
+
+ String[] parts = ace.split(LABEL_SEP_REGEX, 2);
+ hostpart = parts[0];
+ if (parts.length > 1) {
+ domainpart = parts[1];
+ } else {
+ domainpart = "";
+ }
+ }
+
+ public int size() {
+ if (size < 0) {
+ if (isRootLabel()) {
+ size = 1;
+ } else {
+ size = ace.length() + 2;
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public int length() {
+ return ace.length();
+ }
+
+ @Override
+ public char charAt(int index) {
+ return ace.charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return ace.subSequence(start, end);
+ }
+
+ @Override
+ public String toString() {
+ return ace;
+ }
+
+ public static DnsName from(CharSequence name) {
+ return from(name.toString());
+ }
+
+ public static DnsName from(String name) {
+ return new DnsName(name, false);
+ }
+
+ /**
+ * Create a DNS name by "concatenating" the child under the parent name. The child can also be seen as the "left"
+ * part of the resulting DNS name and the parent is the "right" part.
+ *
+ * For example using "i.am.the.child" as child and "of.this.parent.example" as parent, will result in a DNS name:
+ * "i.am.the.child.of.this.parent.example".
+ *
+ *
+ * @param child the child DNS name.
+ * @param parent the parent DNS name.
+ * @return the resulting of DNS name.
+ */
+ public static DnsName from(DnsName child, DnsName parent) {
+ child.setLabelsIfRequired();
+ parent.setLabelsIfRequired();
+
+ DnsLabel[] rawLabels = new DnsLabel[child.rawLabels.length + parent.rawLabels.length];
+ System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
+ System.arraycopy(child.rawLabels, 0, rawLabels, parent.rawLabels.length, child.rawLabels.length);
+ return new DnsName(rawLabels, true);
+ }
+
+ public static DnsName from(DnsLabel child, DnsName parent) {
+ parent.setLabelsIfRequired();
+
+ DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 1];
+ System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
+ rawLabels[rawLabels.length] = child;
+ return new DnsName(rawLabels, true);
+ }
+
+ public static DnsName from(DnsLabel grandchild, DnsLabel child, DnsName parent) {
+ parent.setBytesIfRequired();
+
+ DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 2];
+ System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
+ rawLabels[parent.rawLabels.length] = child;
+ rawLabels[parent.rawLabels.length + 1] = grandchild;
+ return new DnsName(rawLabels, true);
+ }
+
+ public static DnsName from(DnsName... nameComponents) {
+ int labelCount = 0;
+ for (DnsName component : nameComponents) {
+ component.setLabelsIfRequired();
+ labelCount += component.rawLabels.length;
+ }
+
+ DnsLabel[] rawLabels = new DnsLabel[labelCount];
+ int destLabelPos = 0;
+ for (int i = nameComponents.length - 1; i >= 0; i--) {
+ DnsName component = nameComponents[i];
+ System.arraycopy(component.rawLabels, 0, rawLabels, destLabelPos, component.rawLabels.length);
+ destLabelPos += component.rawLabels.length;
+ }
+
+ return new DnsName(rawLabels, true);
+ }
+
+ public static DnsName from(String[] parts) {
+ DnsLabel[] rawLabels = DnsLabel.from(parts);
+
+ return new DnsName(rawLabels, true);
+ }
+
+ /**
+ * Parse a domain name starting at the current offset and moving the input
+ * stream pointer past this domain name (even if cross references occure).
+ *
+ * @param dis The input stream.
+ * @param data The raw data (for cross references).
+ * @return The domain name string.
+ * @throws IOException Should never happen.
+ */
+ public static DnsName parse(DataInputStream dis, byte[] data)
+ throws IOException {
+ int c = dis.readUnsignedByte();
+ if ((c & 0xc0) == 0xc0) {
+ c = ((c & 0x3f) << 8) + dis.readUnsignedByte();
+ HashSet jumps = new HashSet();
+ jumps.add(c);
+ return parse(data, c, jumps);
+ }
+ if (c == 0) {
+ return DnsName.ROOT;
+ }
+ byte[] b = new byte[c];
+ dis.readFully(b);
+
+ String childLabelString = new String(b, StandardCharsets.US_ASCII);
+ DnsName child = new DnsName(childLabelString);
+
+ DnsName parent = parse(dis, data);
+ return DnsName.from(child, parent);
+ }
+
+ /**
+ * Parse a domain name starting at the given offset.
+ *
+ * @param data The raw data.
+ * @param offset The offset.
+ * @param jumps The list of jumps (by now).
+ * @return The parsed domain name.
+ * @throws IllegalStateException on cycles.
+ */
+ private static DnsName parse(byte[] data, int offset, HashSet jumps)
+ throws IllegalStateException {
+ 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 DnsName.ROOT;
+ }
+
+ String childLabelString = new String(data, offset + 1, c, StandardCharsets.US_ASCII);
+ DnsName child = new DnsName(childLabelString);
+
+ DnsName parent = parse(data, offset + 1 + c, jumps);
+ return DnsName.from(child, parent);
+ }
+
+ @Override
+ public int compareTo(DnsName other) {
+ return ace.compareTo(other.ace);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+
+ if (other instanceof DnsName) {
+ DnsName otherDnsName = (DnsName) other;
+ setBytesIfRequired();
+ otherDnsName.setBytesIfRequired();
+ return Arrays.equals(bytes, otherDnsName.bytes);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0 && !isRootLabel()) {
+ setBytesIfRequired();
+ hashCode = Arrays.hashCode(bytes);
+ }
+ return hashCode;
+ }
+
+ public boolean isDirectChildOf(DnsName parent) {
+ setLabelsIfRequired();
+ parent.setLabelsIfRequired();
+ int parentLabelsCount = parent.labels.length;
+
+ if (labels.length - 1 != parentLabelsCount)
+ return false;
+
+ for (int i = 0; i < parent.labels.length; i++) {
+ if (!labels[i].equals(parent.labels[i]))
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean isChildOf(DnsName parent) {
+ setLabelsIfRequired();
+ parent.setLabelsIfRequired();
+
+ if (labels.length < parent.labels.length)
+ return false;
+
+ for (int i = 0; i < parent.labels.length; i++) {
+ if (!labels[i].equals(parent.labels[i]))
+ return false;
+ }
+
+ return true;
+ }
+
+ public int getLabelCount() {
+ setLabelsIfRequired();
+ return labels.length;
+ }
+
+ /**
+ * Get a copy of the labels of this DNS name. The resulting array will contain the labels in reverse order, that is,
+ * the top-level domain will be at res[0].
+ *
+ * @return an array of the labels in reverse order.
+ */
+ public DnsLabel[] getLabels() {
+ setLabelsIfRequired();
+ return labels.clone();
+ }
+
+
+ public DnsLabel getLabel(int labelNum) {
+ setLabelsIfRequired();
+ return labels[labelNum];
+ }
+
+ /**
+ * Get a copy of the raw labels of this DNS name. The resulting array will contain the labels in reverse order, that is,
+ * the top-level domain will be at res[0].
+ *
+ * @return an array of the raw labels in reverse order.
+ */
+ public DnsLabel[] getRawLabels() {
+ setLabelsIfRequired();
+ return rawLabels.clone();
+ }
+
+ public DnsName stripToLabels(int labelCount) {
+ setLabelsIfRequired();
+
+ if (labelCount > labels.length) {
+ throw new IllegalArgumentException();
+ }
+
+ if (labelCount == labels.length) {
+ return this;
+ }
+
+ if (labelCount == 0) {
+ return ROOT;
+ }
+
+ DnsLabel[] stripedLabels = Arrays.copyOfRange(rawLabels, 0, labelCount);
+
+ return new DnsName(stripedLabels, false);
+ }
+
+ /**
+ * Return the parent of this DNS label. Will return the root label if this label itself is the root label (because there is no parent of root).
+ *
+ * For example:
+ *
+ *
+ * "foo.bar.org".getParent() == "bar.org"
+ * ".".getParent() == "."
+ *
+ * @return the parent of this DNS label.
+ */
+ public DnsName getParent() {
+ if (isRootLabel()) return ROOT;
+ return stripToLabels(getLabelCount() - 1);
+ }
+
+ public boolean isRootLabel() {
+ return ace.isEmpty() || ace.equals(".");
+ }
+}
diff --git a/core/java/src/org/minidns/dnsname/InvalidDnsNameException.java b/core/java/src/org/minidns/dnsname/InvalidDnsNameException.java
new file mode 100644
index 000000000..9a2c70969
--- /dev/null
+++ b/core/java/src/org/minidns/dnsname/InvalidDnsNameException.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.dnsname;
+
+import org.minidns.dnslabel.DnsLabel;
+
+public abstract class InvalidDnsNameException extends IllegalStateException {
+
+ private static final long serialVersionUID = 1L;
+
+ protected final String ace;
+
+ protected InvalidDnsNameException(String ace) {
+ this.ace = ace;
+ }
+
+ public static class LabelTooLongException extends InvalidDnsNameException {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ private final String label;
+
+ public LabelTooLongException(String ace, String label) {
+ super(ace);
+ this.label = label;
+ }
+
+ @Override
+ public String getMessage() {
+ return "The DNS name '" + ace + "' contains the label '" + label
+ + "' which exceeds the maximum label length of " + DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS + " octets by "
+ + (label.length() - DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS) + " octets.";
+ }
+ }
+
+ public static class DNSNameTooLongException extends InvalidDnsNameException {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ private final byte[] bytes;
+
+ public DNSNameTooLongException(String ace, byte[] bytes) {
+ super(ace);
+ this.bytes = bytes;
+ }
+
+ @Override
+ public String getMessage() {
+ return "The DNS name '" + ace + "' exceeds the maximum name length of "
+ + DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS + " octets by "
+ + (bytes.length - DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS) + " octets.";
+ }
+ }
+}
diff --git a/core/java/src/org/minidns/edns/Edns.java b/core/java/src/org/minidns/edns/Edns.java
new file mode 100644
index 000000000..f33277d20
--- /dev/null
+++ b/core/java/src/org/minidns/edns/Edns.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.edns;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Data;
+import org.minidns.record.OPT;
+import org.minidns.record.Record;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * EDNS - Extension Mechanism for DNS.
+ *
+ * @see RFC 6891 - Extension Mechanisms for DNS (EDNS(0))
+ *
+ */
+public class Edns {
+
+ /**
+ * Inform the dns server that the client supports DNSSEC.
+ */
+ public static final int FLAG_DNSSEC_OK = 0x8000;
+
+ /**
+ * The EDNS option code.
+ *
+ * @see IANA - DNS EDNS0 Option Codes (OPT)
+ */
+ public enum OptionCode {
+ UNKNOWN(-1, UnknownEdnsOption.class),
+ NSID(3, Nsid.class),
+ ;
+
+ private static Map INVERSE_LUT = new HashMap<>(OptionCode.values().length);
+
+ static {
+ for (OptionCode optionCode : OptionCode.values()) {
+ INVERSE_LUT.put(optionCode.asInt, optionCode);
+ }
+ }
+
+ public final int asInt;
+ public final Class extends EdnsOption> clazz;
+
+ OptionCode(int optionCode, Class extends EdnsOption> clazz) {
+ this.asInt = optionCode;
+ this.clazz = clazz;
+ }
+
+ public static OptionCode from(int optionCode) {
+ OptionCode res = INVERSE_LUT.get(optionCode);
+ if (res == null) res = OptionCode.UNKNOWN;
+ return res;
+ }
+ }
+
+ public final int udpPayloadSize;
+
+ /**
+ * 8-bit extended return code.
+ *
+ * RFC 6891 § 6.1.3 EXTENDED-RCODE
+ */
+ public final int extendedRcode;
+
+ /**
+ * 8-bit version field.
+ *
+ * RFC 6891 § 6.1.3 VERSION
+ */
+ public final int version;
+
+ /**
+ * 16-bit flags.
+ *
+ * RFC 6891 § 6.1.4
+ */
+ public final int flags;
+
+ public final List variablePart;
+
+ public final boolean dnssecOk;
+
+ private Record optRecord;
+
+ public Edns(Record optRecord) {
+ assert optRecord.type == TYPE.OPT;
+ udpPayloadSize = optRecord.clazzValue;
+ extendedRcode = (int) ((optRecord.ttl >> 8) & 0xff);
+ version = (int) ((optRecord.ttl >> 16) & 0xff);
+ flags = (int) optRecord.ttl & 0xffff;
+
+ dnssecOk = (optRecord.ttl & FLAG_DNSSEC_OK) > 0;
+
+ OPT opt = optRecord.payloadData;
+ variablePart = opt.variablePart;
+ this.optRecord = optRecord;
+ }
+
+ public Edns(Builder builder) {
+ udpPayloadSize = builder.udpPayloadSize;
+ extendedRcode = builder.extendedRcode;
+ version = builder.version;
+ int flags = 0;
+ if (builder.dnssecOk) {
+ flags |= FLAG_DNSSEC_OK;
+ }
+ dnssecOk = builder.dnssecOk;
+ this.flags = flags;
+ if (builder.variablePart != null) {
+ variablePart = builder.variablePart;
+ } else {
+ variablePart = Collections.emptyList();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public O getEdnsOption(OptionCode optionCode) {
+ for (EdnsOption o : variablePart) {
+ if (o.getOptionCode().equals(optionCode)) {
+ return (O) o;
+ }
+ }
+ return null;
+ }
+
+ public Record asRecord() {
+ if (optRecord == null) {
+ long optFlags = flags;
+ optFlags |= extendedRcode << 8;
+ optFlags |= version << 16;
+ optRecord = new Record(DnsName.ROOT, Record.TYPE.OPT, udpPayloadSize, optFlags, new OPT(variablePart));
+ }
+ return optRecord;
+ }
+
+ private String terminalOutputCache;
+
+ public String asTerminalOutput() {
+ if (terminalOutputCache == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("EDNS: version: ").append(version).append(", flags:");
+ if (dnssecOk)
+ sb.append(" do");
+ sb.append("; udp: ").append(udpPayloadSize);
+ if (!variablePart.isEmpty()) {
+ sb.append('\n');
+ Iterator it = variablePart.iterator();
+ while (it.hasNext()) {
+ EdnsOption edns = it.next();
+ sb.append(edns.getOptionCode()).append(": ");
+ sb.append(edns.asTerminalOutput());
+ if (it.hasNext()) {
+ sb.append('\n');
+ }
+ }
+ }
+ terminalOutputCache = sb.toString();
+ }
+ return terminalOutputCache;
+ }
+
+ @Override
+ public String toString() {
+ return asTerminalOutput();
+ }
+
+ public static Edns fromRecord(Record extends Data> record) {
+ if (record.type != TYPE.OPT) return null;
+
+ @SuppressWarnings("unchecked")
+ Record optRecord = (Record) record;
+ return new Edns(optRecord);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private int udpPayloadSize;
+ private int extendedRcode;
+ private int version;
+ private boolean dnssecOk;
+ private List variablePart;
+
+ private Builder() {
+ }
+
+ public Builder setUdpPayloadSize(int udpPayloadSize) {
+ if (udpPayloadSize > 0xffff) {
+ throw new IllegalArgumentException("UDP payload size must not be greater than 65536, was " + udpPayloadSize);
+ }
+ this.udpPayloadSize = udpPayloadSize;
+ return this;
+ }
+
+ public Builder setDnssecOk(boolean dnssecOk) {
+ this.dnssecOk = dnssecOk;
+ return this;
+ }
+
+ public Builder setDnssecOk() {
+ dnssecOk = true;
+ return this;
+ }
+
+ public Builder addEdnsOption(EdnsOption ednsOption) {
+ if (variablePart == null) {
+ variablePart = new ArrayList<>(4);
+ }
+ variablePart.add(ednsOption);
+ return this;
+ }
+
+ public Edns build() {
+ return new Edns(this);
+ }
+ }
+}
diff --git a/core/java/src/org/minidns/edns/EdnsOption.java b/core/java/src/org/minidns/edns/EdnsOption.java
new file mode 100644
index 000000000..b6396526c
--- /dev/null
+++ b/core/java/src/org/minidns/edns/EdnsOption.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.edns;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.minidns.edns.Edns.OptionCode;
+
+public abstract class EdnsOption {
+
+ public final int optionCode;
+ public final int optionLength;
+
+ protected final byte[] optionData;
+
+ protected EdnsOption(int optionCode, byte[] optionData) {
+ this.optionCode = optionCode;
+ this.optionLength = optionData.length;
+ this.optionData = optionData;
+ }
+
+ protected EdnsOption(byte[] optionData) {
+ this.optionCode = getOptionCode().asInt;
+ this.optionLength = optionData.length;
+ this.optionData = optionData;
+ }
+
+ public final void writeToDos(DataOutputStream dos) throws IOException {
+ dos.writeShort(optionCode);
+ dos.writeShort(optionLength);
+ dos.write(optionData);
+ }
+
+ public abstract OptionCode getOptionCode();
+
+ private String toStringCache;
+
+ @Override
+ public final String toString() {
+ if (toStringCache == null) {
+ toStringCache = toStringInternal().toString();
+ }
+ return toStringCache;
+ }
+
+ protected abstract CharSequence toStringInternal();
+
+ private String terminalOutputCache;
+
+ public final String asTerminalOutput() {
+ if (terminalOutputCache == null) {
+ terminalOutputCache = asTerminalOutputInternal().toString();
+ }
+ return terminalOutputCache;
+ }
+
+ protected abstract CharSequence asTerminalOutputInternal();
+
+ public static EdnsOption parse(int intOptionCode, byte[] optionData) {
+ OptionCode optionCode = OptionCode.from(intOptionCode);
+ EdnsOption res;
+ switch (optionCode) {
+ case NSID:
+ res = new Nsid(optionData);
+ break;
+ default:
+ res = new UnknownEdnsOption(intOptionCode, optionData);
+ break;
+ }
+ return res;
+ }
+
+}
diff --git a/core/java/src/org/minidns/edns/Nsid.java b/core/java/src/org/minidns/edns/Nsid.java
new file mode 100644
index 000000000..4e68c9261
--- /dev/null
+++ b/core/java/src/org/minidns/edns/Nsid.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.edns;
+
+import java.nio.charset.StandardCharsets;
+
+import org.minidns.edns.Edns.OptionCode;
+import org.minidns.util.Hex;
+
+public class Nsid extends EdnsOption {
+
+ public static final Nsid REQUEST = new Nsid();
+
+ private Nsid() {
+ this(new byte[0]);
+ }
+
+ public Nsid(byte[] payload) {
+ super(payload);
+ }
+
+ @Override
+ public OptionCode getOptionCode() {
+ return OptionCode.NSID;
+ }
+
+ @Override
+ protected CharSequence toStringInternal() {
+ String res = OptionCode.NSID + ": ";
+ res += new String(optionData, StandardCharsets.US_ASCII);
+ return res;
+ }
+
+ @Override
+ protected CharSequence asTerminalOutputInternal() {
+ return Hex.from(optionData);
+ }
+
+}
diff --git a/core/java/src/org/minidns/edns/UnknownEdnsOption.java b/core/java/src/org/minidns/edns/UnknownEdnsOption.java
new file mode 100644
index 000000000..d9bead8bd
--- /dev/null
+++ b/core/java/src/org/minidns/edns/UnknownEdnsOption.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.edns;
+
+import org.minidns.edns.Edns.OptionCode;
+import org.minidns.util.Hex;
+
+public class UnknownEdnsOption extends EdnsOption {
+
+ protected UnknownEdnsOption(int optionCode, byte[] optionData) {
+ super(optionCode, optionData);
+ }
+
+ @Override
+ public OptionCode getOptionCode() {
+ return OptionCode.UNKNOWN;
+ }
+
+ @Override
+ protected CharSequence asTerminalOutputInternal() {
+ return Hex.from(optionData);
+ }
+
+ @Override
+ protected CharSequence toStringInternal() {
+ return asTerminalOutputInternal();
+ }
+
+}
diff --git a/core/java/src/org/minidns/idna/DefaultIdnaTransformator.java b/core/java/src/org/minidns/idna/DefaultIdnaTransformator.java
new file mode 100644
index 000000000..404a50887
--- /dev/null
+++ b/core/java/src/org/minidns/idna/DefaultIdnaTransformator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.idna;
+
+import java.net.IDN;
+
+import org.minidns.dnsname.DnsName;
+
+public class DefaultIdnaTransformator implements IdnaTransformator {
+
+ @Override
+ public String toASCII(String input) {
+ // Special case if input is ".", i.e. a string containing only a single dot character. This is a workaround for
+ // IDN.toASCII() implementations throwing an IllegalArgumentException on this input string (for example Android
+ // APIs level 26, see https://issuetracker.google.com/issues/113070416).
+ if (DnsName.ROOT.ace.equals(input)) {
+ return DnsName.ROOT.ace;
+ }
+
+ return IDN.toASCII(input);
+ }
+
+ @Override
+ public String toUnicode(String input) {
+ return IDN.toUnicode(input);
+ }
+
+}
diff --git a/core/java/src/org/minidns/idna/IdnaTransformator.java b/core/java/src/org/minidns/idna/IdnaTransformator.java
new file mode 100644
index 000000000..66c98e25f
--- /dev/null
+++ b/core/java/src/org/minidns/idna/IdnaTransformator.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.idna;
+
+public interface IdnaTransformator {
+
+ String toASCII(String input);
+
+ String toUnicode(String input);
+
+}
diff --git a/core/java/src/org/minidns/idna/MiniDnsIdna.java b/core/java/src/org/minidns/idna/MiniDnsIdna.java
new file mode 100644
index 000000000..2e0d1e110
--- /dev/null
+++ b/core/java/src/org/minidns/idna/MiniDnsIdna.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.idna;
+
+public class MiniDnsIdna {
+
+ private static IdnaTransformator idnaTransformator = new DefaultIdnaTransformator();
+
+ public static String toASCII(String string) {
+ return idnaTransformator.toASCII(string);
+ }
+
+ public static String toUnicode(String string) {
+ return idnaTransformator.toUnicode(string);
+ }
+
+ public static void setActiveTransformator(IdnaTransformator idnaTransformator) {
+ if (idnaTransformator == null) {
+ throw new IllegalArgumentException();
+ }
+ MiniDnsIdna.idnaTransformator = idnaTransformator;
+ }
+}
diff --git a/core/java/src/org/minidns/package.html b/core/java/src/org/minidns/package.html
new file mode 100644
index 000000000..fa2aa829a
--- /dev/null
+++ b/core/java/src/org/minidns/package.html
@@ -0,0 +1,16 @@
+
+
+This is a portion of release 1.0.0 of MiniDNS from https://github.com/MiniDNS/minidns/ 2020-07-18
+Only contains the minidns-core portion of the library.
+Removed tests, most util classes, and DnsRootServer class.
+
+
+
+This software may be used under the terms of (at your choice)
+- LGPL version 2 (or later) (see LICENCE_LGPL2.1 for details)
+- Apache Software licence (see LICENCE_APACHE for details)
+- WTFPL (see LICENCE_WTFPL for details)
+
+
+
+
diff --git a/core/java/src/org/minidns/record/A.java b/core/java/src/org/minidns/record/A.java
new file mode 100644
index 000000000..6771daecb
--- /dev/null
+++ b/core/java/src/org/minidns/record/A.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.net.Inet4Address;
+
+import org.minidns.record.Record.TYPE;
+import org.minidns.util.InetAddressUtil;
+
+/**
+ * A record payload (ip pointer).
+ */
+public class A extends InternetAddressRR {
+
+ @Override
+ public TYPE getType() {
+ return TYPE.A;
+ }
+
+ public A(Inet4Address inet4Address) {
+ super(inet4Address);
+ assert ip.length == 4;
+ }
+
+ public A(int q1, int q2, int q3, int q4) {
+ super(new byte[] { (byte) q1, (byte) q2, (byte) q3, (byte) q4 });
+ if (q1 < 0 || q1 > 255 || q2 < 0 || q2 > 255 || q3 < 0 || q3 > 255 || q4 < 0 || q4 > 255) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public A(byte[] ip) {
+ super(ip);
+ if (ip.length != 4) {
+ throw new IllegalArgumentException("IPv4 address in A record is always 4 byte");
+ }
+ }
+
+ public A(CharSequence ipv4CharSequence) {
+ this(InetAddressUtil.ipv4From(ipv4CharSequence));
+ }
+
+ public static A parse(DataInputStream dis)
+ throws IOException {
+ byte[] ip = new byte[4];
+ dis.readFully(ip);
+ return new A(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/core/java/src/org/minidns/record/AAAA.java b/core/java/src/org/minidns/record/AAAA.java
new file mode 100644
index 000000000..3ac78213c
--- /dev/null
+++ b/core/java/src/org/minidns/record/AAAA.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.net.Inet6Address;
+
+import org.minidns.record.Record.TYPE;
+import org.minidns.util.InetAddressUtil;
+
+/**
+ * AAAA payload (an ipv6 pointer).
+ */
+public class AAAA extends InternetAddressRR {
+
+ @Override
+ public TYPE getType() {
+ return TYPE.AAAA;
+ }
+
+ public AAAA(Inet6Address inet6address) {
+ super(inet6address);
+ assert ip.length == 16;
+ }
+
+ public AAAA(byte[] ip) {
+ super(ip);
+ if (ip.length != 16) {
+ throw new IllegalArgumentException("IPv6 address in AAAA record is always 16 byte");
+ }
+ }
+
+ public AAAA(CharSequence ipv6CharSequence) {
+ this(InetAddressUtil.ipv6From(ipv6CharSequence));
+ }
+
+ public static AAAA parse(DataInputStream dis)
+ throws IOException {
+ byte[] ip = new byte[16];
+ dis.readFully(ip);
+ return new AAAA(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/core/java/src/org/minidns/record/CNAME.java b/core/java/src/org/minidns/record/CNAME.java
new file mode 100644
index 000000000..7b5c6aeee
--- /dev/null
+++ b/core/java/src/org/minidns/record/CNAME.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * CNAME payload (pointer to another domain / address).
+ */
+public class CNAME extends RRWithTarget {
+
+ public static CNAME parse(DataInputStream dis, byte[] data) throws IOException {
+ DnsName target = DnsName.parse(dis, data);
+ return new CNAME(target);
+ }
+
+ public CNAME(String target) {
+ this(DnsName.from(target));
+ }
+
+ public CNAME(DnsName target) {
+ super(target);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.CNAME;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/DLV.java b/core/java/src/org/minidns/record/DLV.java
new file mode 100644
index 000000000..ccd94de3f
--- /dev/null
+++ b/core/java/src/org/minidns/record/DLV.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.minidns.constants.DnssecConstants.DigestAlgorithm;
+import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
+
+/**
+ * DLV record payload.
+ *
+ * According to RFC4431, DLV has exactly the same format as DS records.
+ */
+public class DLV extends DelegatingDnssecRR {
+
+ public static DLV parse (DataInputStream dis, int length) throws IOException {
+ SharedData parsedData = DelegatingDnssecRR.parseSharedData(dis, length);
+ return new DLV(parsedData.keyTag, parsedData.algorithm, parsedData.digestType, parsedData.digest);
+ }
+
+ public DLV(int keyTag, byte algorithm, byte digestType, byte[] digest) {
+ super(keyTag, algorithm, digestType, digest);
+ }
+
+ public DLV(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) {
+ super(keyTag, algorithm, digestType, digest);
+ }
+
+ @Override
+ public Record.TYPE getType() {
+ return Record.TYPE.DLV;
+ }
+}
diff --git a/core/java/src/org/minidns/record/DNAME.java b/core/java/src/org/minidns/record/DNAME.java
new file mode 100644
index 000000000..bdfe1b1ca
--- /dev/null
+++ b/core/java/src/org/minidns/record/DNAME.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * A DNAME resource record.
+ *
+ * @see RFC 6672 - DNAME Redirection in the DNS
+ */
+public class DNAME extends RRWithTarget {
+
+ public static DNAME parse(DataInputStream dis, byte[] data) throws IOException {
+ DnsName target = DnsName.parse(dis, data);
+ return new DNAME(target);
+ }
+
+ public DNAME(String target) {
+ this(DnsName.from(target));
+ }
+
+ public DNAME(DnsName target) {
+ super(target);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.DNAME;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/DNSKEY.java b/core/java/src/org/minidns/record/DNSKEY.java
new file mode 100644
index 000000000..12d4cbf46
--- /dev/null
+++ b/core/java/src/org/minidns/record/DNSKEY.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
+import org.minidns.record.Record.TYPE;
+import org.minidns.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+
+/**
+ * DNSKEY record payload.
+ */
+public class DNSKEY extends Data {
+ /**
+ * Whether the key should be used as a secure entry point key.
+ *
+ * see RFC 3757
+ */
+ public static final short FLAG_SECURE_ENTRY_POINT = 0x1;
+
+ /**
+ * Whether the record holds a revoked key.
+ */
+ public static final short FLAG_REVOKE = 0x80;
+
+ /**
+ * Whether the record holds a DNS zone key.
+ */
+ public static final short FLAG_ZONE = 0x100;
+
+ /**
+ * Use the protocol defined in RFC 4034.
+ */
+ public static final byte PROTOCOL_RFC4034 = 3;
+
+ /**
+ * Bitmap of flags: {@link #FLAG_SECURE_ENTRY_POINT}, {@link #FLAG_REVOKE}, {@link #FLAG_ZONE}.
+ *
+ * @see IANA - DNSKEY RR Flags
+ */
+ public final short flags;
+
+ /**
+ * Must be {@link #PROTOCOL_RFC4034}.
+ */
+ public final byte protocol;
+
+ /**
+ * The public key's cryptographic algorithm used.
+ *
+ */
+ public final SignatureAlgorithm algorithm;
+
+ /**
+ * The byte value of the public key's cryptographic algorithm used.
+ *
+ */
+ public final byte algorithmByte;
+
+ /**
+ * The public key material. The format depends on the algorithm of the key being stored.
+ */
+ private final byte[] key;
+
+ /**
+ * This DNSKEY's key tag. Calculated just-in-time when using {@link #getKeyTag()}
+ */
+ private transient Integer keyTag;
+
+ public static DNSKEY parse(DataInputStream dis, int length) throws IOException {
+ short flags = dis.readShort();
+ byte protocol = dis.readByte();
+ byte algorithm = dis.readByte();
+ byte[] key = new byte[length - 4];
+ dis.readFully(key);
+ return new DNSKEY(flags, protocol, algorithm, key);
+ }
+
+ private DNSKEY(short flags, byte protocol, SignatureAlgorithm algorithm, byte algorithmByte, byte[] key) {
+ this.flags = flags;
+ this.protocol = protocol;
+
+ assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte);
+ this.algorithmByte = algorithmByte;
+ this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte);
+
+ this.key = key;
+ }
+
+ public DNSKEY(short flags, byte protocol, byte algorithm, byte[] key) {
+ this(flags, protocol, SignatureAlgorithm.forByte(algorithm), key);
+ }
+
+ public DNSKEY(short flags, byte protocol, SignatureAlgorithm algorithm, byte[] key) {
+ this(flags, protocol, algorithm, algorithm.number, key);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.DNSKEY;
+ }
+
+ /**
+ * Retrieve the key tag identifying this DNSKEY.
+ * The key tag is used within the DS and RRSIG record to distinguish multiple keys for the same name.
+ *
+ * This implementation is based on the reference implementation shown in RFC 4034 Appendix B.
+ *
+ * @return this DNSKEY's key tag
+ */
+ public /* unsigned short */ int getKeyTag() {
+ if (keyTag == null) {
+ byte[] recordBytes = toByteArray();
+ long ac = 0;
+
+ for (int i = 0; i < recordBytes.length; ++i) {
+ ac += ((i & 1) > 0) ? recordBytes[i] & 0xFFL : ((recordBytes[i] & 0xFFL) << 8);
+ }
+ ac += (ac >> 16) & 0xFFFF;
+ keyTag = (int) (ac & 0xFFFF);
+ }
+ return keyTag;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeShort(flags);
+ dos.writeByte(protocol);
+ dos.writeByte(algorithm.number);
+ dos.write(key);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(flags).append(' ')
+ .append(protocol).append(' ')
+ .append(algorithm).append(' ')
+ .append(Base64.encodeToString(key));
+ return sb.toString();
+ }
+
+ public int getKeyLength() {
+ return key.length;
+ }
+
+ public byte[] getKey() {
+ return key.clone();
+ }
+
+ public DataInputStream getKeyAsDataInputStream() {
+ return new DataInputStream(new ByteArrayInputStream(key));
+ }
+
+ private transient String keyBase64Cache;
+
+ public String getKeyBase64() {
+ if (keyBase64Cache == null) {
+ keyBase64Cache = Base64.encodeToString(key);
+ }
+ return keyBase64Cache;
+ }
+
+ private transient BigInteger keyBigIntegerCache;
+
+ public BigInteger getKeyBigInteger() {
+ if (keyBigIntegerCache == null) {
+ keyBigIntegerCache = new BigInteger(key);
+ }
+ return keyBigIntegerCache;
+ }
+
+ public boolean keyEquals(byte[] otherKey) {
+ return Arrays.equals(key, otherKey);
+ }
+
+ public boolean isSecureEntryPoint() {
+ return (flags & FLAG_SECURE_ENTRY_POINT) == 1;
+ }
+}
diff --git a/core/java/src/org/minidns/record/DS.java b/core/java/src/org/minidns/record/DS.java
new file mode 100644
index 000000000..56bca28be
--- /dev/null
+++ b/core/java/src/org/minidns/record/DS.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.constants.DnssecConstants.DigestAlgorithm;
+import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+/**
+ * DS (Delegation Signer) record payload.
+ *
+ * @see RFC 4034 § 5
+ */
+public class DS extends DelegatingDnssecRR {
+
+ public static DS parse(DataInputStream dis, int length) throws IOException {
+ SharedData parsedData = DelegatingDnssecRR.parseSharedData(dis, length);
+ return new DS(parsedData.keyTag, parsedData.algorithm, parsedData.digestType, parsedData.digest);
+ }
+
+ public DS(int keyTag, byte algorithm, byte digestType, byte[] digest) {
+ super(keyTag, algorithm, digestType, digest);
+ }
+
+ public DS(int keyTag, SignatureAlgorithm algorithm, byte digestType, byte[] digest) {
+ super(keyTag, algorithm, digestType, digest);
+ }
+
+ public DS(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) {
+ super(keyTag, algorithm, digestType, digest);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.DS;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/Data.java b/core/java/src/org/minidns/record/Data.java
new file mode 100644
index 000000000..3ae522435
--- /dev/null
+++ b/core/java/src/org/minidns/record/Data.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import org.minidns.record.Record.TYPE;
+
+/**
+ * Generic payload class.
+ */
+public abstract class Data {
+
+ /**
+ * The payload type.
+ * @return The payload type.
+ */
+ public abstract TYPE getType();
+
+ /**
+ * The internal method used to serialize Data subclasses.
+ *
+ * @param dos the output stream to serialize to.
+ * @throws IOException if an I/O error occurs.
+ */
+ protected abstract void serialize(DataOutputStream dos) throws IOException;
+
+ private byte[] bytes;
+
+ private void setBytes() {
+ if (bytes != null) return;
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+ try {
+ serialize(dos);
+ } catch (IOException e) {
+ // Should never happen.
+ throw new AssertionError(e);
+ }
+ bytes = baos.toByteArray();
+ }
+
+ public final int length() {
+ setBytes();
+ return bytes.length;
+ }
+
+ public final void toOutputStream(OutputStream outputStream) throws IOException {
+ DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
+ toOutputStream(dataOutputStream);
+ }
+
+ /**
+ * Write the binary representation of this payload to the given {@link DataOutputStream}.
+ *
+ * @param dos the DataOutputStream to write to.
+ * @throws IOException if an I/O error occurs.
+ */
+ public final void toOutputStream(DataOutputStream dos) throws IOException {
+ setBytes();
+ dos.write(bytes);
+ }
+
+ public final byte[] toByteArray() {
+ setBytes();
+ return bytes.clone();
+ }
+
+ private transient Integer hashCodeCache;
+
+ @Override
+ public final int hashCode() {
+ if (hashCodeCache == null) {
+ setBytes();
+ hashCodeCache = Arrays.hashCode(bytes);
+ }
+ return hashCodeCache;
+ }
+
+ @Override
+ public final boolean equals(Object other) {
+ if (!(other instanceof Data)) {
+ return false;
+ }
+ if (other == this) {
+ return true;
+ }
+ Data otherData = (Data) other;
+ otherData.setBytes();
+ setBytes();
+
+ return Arrays.equals(bytes, otherData.bytes);
+ }
+}
diff --git a/core/java/src/org/minidns/record/DelegatingDnssecRR.java b/core/java/src/org/minidns/record/DelegatingDnssecRR.java
new file mode 100644
index 000000000..44b67f3cd
--- /dev/null
+++ b/core/java/src/org/minidns/record/DelegatingDnssecRR.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+
+import org.minidns.constants.DnssecConstants.DigestAlgorithm;
+import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
+
+/**
+ * DS (Delegation Signer) record payload.
+ *
+ * @see RFC 4034 § 5
+ */
+public abstract class DelegatingDnssecRR extends Data {
+
+ /**
+ * The key tag value of the DNSKEY RR that validates this signature.
+ */
+ public final int /* unsigned short */ keyTag;
+
+ /**
+ * The cryptographic algorithm used to create the signature. If MiniDNS
+ * isn't aware of the signature algorithm, then this field will be
+ * null.
+ *
+ * @see #algorithmByte
+ */
+ public final SignatureAlgorithm algorithm;
+
+ /**
+ * The byte value of the cryptographic algorithm used to create the signature.
+ */
+ public final byte algorithmByte;
+
+ /**
+ * The algorithm used to construct the digest. If MiniDNS
+ * isn't aware of the digest algorithm, then this field will be
+ * null.
+ *
+ * @see #digestTypeByte
+ */
+ public final DigestAlgorithm digestType;
+
+ /**
+ * The byte value of algorithm used to construct the digest.
+ */
+ public final byte digestTypeByte;
+
+ /**
+ * The digest build from a DNSKEY.
+ */
+ protected final byte[] digest;
+
+ protected static SharedData parseSharedData(DataInputStream dis, int length) throws IOException {
+ int keyTag = dis.readUnsignedShort();
+ byte algorithm = dis.readByte();
+ byte digestType = dis.readByte();
+ byte[] digest = new byte[length - 4];
+ if (dis.read(digest) != digest.length) throw new IOException();
+ return new SharedData(keyTag, algorithm, digestType, digest);
+ }
+
+ protected static final class SharedData {
+ protected final int keyTag;
+ protected final byte algorithm;
+ protected final byte digestType;
+ protected final byte[] digest;
+
+ private SharedData(int keyTag, byte algorithm, byte digestType, byte[] digest) {
+ this.keyTag = keyTag;
+ this.algorithm = algorithm;
+ this.digestType = digestType;
+ this.digest = digest;
+ }
+ }
+
+ protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, byte algorithmByte, DigestAlgorithm digestType, byte digestTypeByte, byte[] digest) {
+ this.keyTag = keyTag;
+
+ assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte);
+ this.algorithmByte = algorithmByte;
+ this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte);
+
+ assert digestTypeByte == (digestType != null ? digestType.value : digestTypeByte);
+ this.digestTypeByte = digestTypeByte;
+ this.digestType = digestType != null ? digestType : DigestAlgorithm.forByte(digestTypeByte);
+
+ assert digest != null;
+ this.digest = digest;
+ }
+
+ protected DelegatingDnssecRR(int keyTag, byte algorithm, byte digestType, byte[] digest) {
+ this(keyTag, null, algorithm, null, digestType, digest);
+ }
+
+ protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) {
+ this(keyTag, algorithm, algorithm.number, digestType, digestType.value, digest);
+ }
+
+ protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, byte digestType, byte[] digest) {
+ this(keyTag, algorithm, algorithm.number, null, digestType, digest);
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeShort(keyTag);
+ dos.writeByte(algorithmByte);
+ dos.writeByte(digestTypeByte);
+ dos.write(digest);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(keyTag).append(' ')
+ .append(algorithm).append(' ')
+ .append(digestType).append(' ')
+ .append(new BigInteger(1, digest).toString(16).toUpperCase());
+ return sb.toString();
+ }
+
+ private transient BigInteger digestBigIntCache;
+
+ public BigInteger getDigestBigInteger() {
+ if (digestBigIntCache == null) {
+ digestBigIntCache = new BigInteger(1, digest);
+ }
+ return digestBigIntCache;
+ }
+
+ private transient String digestHexCache;
+
+ public String getDigestHex() {
+ if (digestHexCache == null) {
+ digestHexCache = getDigestBigInteger().toString(16).toUpperCase();
+ }
+ return digestHexCache;
+ }
+
+ public boolean digestEquals(byte[] otherDigest) {
+ return Arrays.equals(digest, otherDigest);
+ }
+}
diff --git a/core/java/src/org/minidns/record/InternetAddressRR.java b/core/java/src/org/minidns/record/InternetAddressRR.java
new file mode 100644
index 000000000..fa51b45e8
--- /dev/null
+++ b/core/java/src/org/minidns/record/InternetAddressRR.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * A resource record representing a internet address. Provides {@link #getInetAddress()}.
+ */
+public abstract class InternetAddressRR extends Data {
+
+
+ /**
+ * Target IP.
+ */
+ protected final byte[] ip;
+
+ /**
+ * Cache for the {@link InetAddress} this record presents.
+ */
+ private transient IA inetAddress;
+
+ protected InternetAddressRR(byte[] ip) {
+ this.ip = ip;
+ }
+
+ protected InternetAddressRR(IA inetAddress) {
+ this(inetAddress.getAddress());
+ this.inetAddress = inetAddress;
+ }
+
+ @Override
+ public final void serialize(DataOutputStream dos) throws IOException {
+ dos.write(ip);
+ }
+
+ /**
+ * Allocates a new byte buffer and fills the buffer with the bytes representing the IP address of this resource record.
+ *
+ * @return a new byte buffer containing the bytes of the IP.
+ */
+ public final byte[] getIp() {
+ return ip.clone();
+ }
+
+ @SuppressWarnings("unchecked")
+ public final IA getInetAddress() {
+ if (inetAddress == null) {
+ try {
+ inetAddress = (IA) InetAddress.getByAddress(ip);
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return inetAddress;
+ }
+
+ public static InternetAddressRR extends InetAddress> from(InetAddress inetAddress) {
+ if (inetAddress instanceof Inet4Address) {
+ return new A((Inet4Address) inetAddress);
+ }
+
+ return new AAAA((Inet6Address) inetAddress);
+ }
+}
diff --git a/core/java/src/org/minidns/record/MX.java b/core/java/src/org/minidns/record/MX.java
new file mode 100644
index 000000000..c4001856a
--- /dev/null
+++ b/core/java/src/org/minidns/record/MX.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * MX record payload (mail service pointer).
+ */
+public class MX extends Data {
+
+ /**
+ * The priority of this service. Lower values mean higher priority.
+ */
+ public final int priority;
+
+ /**
+ * The name of the target server.
+ */
+ public final DnsName target;
+
+ /**
+ * The name of the target server.
+ *
+ * @deprecated use {@link #target} instead.
+ */
+ @Deprecated
+ public final DnsName name;
+
+ public static MX parse(DataInputStream dis, byte[] data)
+ throws IOException {
+ int priority = dis.readUnsignedShort();
+ DnsName name = DnsName.parse(dis, data);
+ return new MX(priority, name);
+ }
+
+ public MX(int priority, String name) {
+ this(priority, DnsName.from(name));
+ }
+
+ public MX(int priority, DnsName name) {
+ this.priority = priority;
+ this.target = name;
+ this.name = target;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeShort(priority);
+ target.writeToStream(dos);
+ }
+
+ @Override
+ public String toString() {
+ return priority + " " + target + '.';
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.MX;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/NS.java b/core/java/src/org/minidns/record/NS.java
new file mode 100644
index 000000000..69f402c05
--- /dev/null
+++ b/core/java/src/org/minidns/record/NS.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * Nameserver record.
+ */
+public class NS extends RRWithTarget {
+
+ public static NS parse(DataInputStream dis, byte[] data) throws IOException {
+ DnsName target = DnsName.parse(dis, data);
+ return new NS(target);
+ }
+
+ public NS(DnsName name) {
+ super(name);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.NS;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/NSEC.java b/core/java/src/org/minidns/record/NSEC.java
new file mode 100644
index 000000000..a54778389
--- /dev/null
+++ b/core/java/src/org/minidns/record/NSEC.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * NSEC record payload.
+ */
+public class NSEC extends Data {
+
+ private static final Logger LOGGER = Logger.getLogger(NSEC.class.getName());
+
+ /**
+ * The next owner name that contains a authoritative data or a delegation point.
+ */
+ public final DnsName next;
+
+ private final byte[] typeBitmap;
+
+ /**
+ * The RR types existing at the owner name.
+ */
+ public final List types;
+
+ public static NSEC parse(DataInputStream dis, byte[] data, int length) throws IOException {
+ DnsName next = DnsName.parse(dis, data);
+
+ byte[] typeBitmap = new byte[length - next.size()];
+ if (dis.read(typeBitmap) != typeBitmap.length) throw new IOException();
+ List types = readTypeBitMap(typeBitmap);
+ return new NSEC(next, types);
+ }
+
+ public NSEC(String next, List types) {
+ this(DnsName.from(next), types);
+ }
+
+ public NSEC(String next, TYPE... types) {
+ this(DnsName.from(next), Arrays.asList(types));
+ }
+
+ public NSEC(DnsName next, List types) {
+ this.next = next;
+ this.types = Collections.unmodifiableList(types);
+ this.typeBitmap = createTypeBitMap(types);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.NSEC;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ next.writeToStream(dos);
+ dos.write(typeBitmap);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(next).append('.');
+ for (TYPE type : types) {
+ sb.append(' ').append(type);
+ }
+ return sb.toString();
+ }
+
+ @SuppressWarnings("NarrowingCompoundAssignment")
+ static byte[] createTypeBitMap(List types) {
+ List typeList = new ArrayList(types.size());
+ for (TYPE type : types) {
+ typeList.add(type.getValue());
+ }
+ Collections.sort(typeList);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ try {
+ int windowBlock = -1;
+ byte[] bitmap = null;
+ for (Integer type : typeList) {
+ if (windowBlock == -1 || (type >> 8) != windowBlock) {
+ if (windowBlock != -1) writeOutBlock(bitmap, dos);
+ windowBlock = type >> 8;
+ dos.writeByte(windowBlock);
+ bitmap = new byte[32];
+ }
+ int a = (type >> 3) % 32;
+ int b = type % 8;
+ bitmap[a] |= 128 >> b;
+ }
+ if (windowBlock != -1) writeOutBlock(bitmap, dos);
+ } catch (IOException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+
+ return baos.toByteArray();
+ }
+
+ private static void writeOutBlock(byte[] values, DataOutputStream dos) throws IOException {
+ int n = 0;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] != 0) n = i + 1;
+ }
+ dos.writeByte(n);
+ for (int i = 0; i < n; i++) {
+ dos.writeByte(values[i]);
+ }
+ }
+
+ // TODO: This method should probably just return List so that unknown types can be act on later.
+ static List readTypeBitMap(byte[] typeBitmap) throws IOException {
+ DataInputStream dis = new DataInputStream(new ByteArrayInputStream(typeBitmap));
+ int read = 0;
+ ArrayList typeList = new ArrayList();
+ while (typeBitmap.length > read) {
+ int windowBlock = dis.readUnsignedByte();
+ int bitmapLength = dis.readUnsignedByte();
+ for (int i = 0; i < bitmapLength; i++) {
+ int b = dis.readUnsignedByte();
+ for (int j = 0; j < 8; j++) {
+ if (((b >> j) & 0x1) > 0) {
+ int typeInt = (windowBlock << 8) + (i * 8) + (7 - j);
+ TYPE type = TYPE.getType(typeInt);
+ if (type == TYPE.UNKNOWN) {
+ LOGGER.warning("Skipping unknown type in type bitmap: " + typeInt);
+ continue;
+ }
+ typeList.add(type);
+ }
+ }
+ }
+ read += bitmapLength + 2;
+ }
+ return typeList;
+ }
+}
diff --git a/core/java/src/org/minidns/record/NSEC3.java b/core/java/src/org/minidns/record/NSEC3.java
new file mode 100644
index 000000000..629e38b99
--- /dev/null
+++ b/core/java/src/org/minidns/record/NSEC3.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.dnslabel.DnsLabel;
+import org.minidns.record.Record.TYPE;
+import org.minidns.util.Base32;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NSEC3 record payload.
+ */
+public class NSEC3 extends Data {
+
+ /**
+ * This Flag indicates whether this NSEC3 RR may cover unsigned
+ * delegations.
+ */
+ public static final byte FLAG_OPT_OUT = 0x1;
+
+ private static final Map HASH_ALGORITHM_LUT = new HashMap<>();
+
+ /**
+ * DNSSEC NSEC3 Hash Algorithms.
+ *
+ * @see
+ * IANA DNSSEC NSEC3 Hash Algorithms
+ */
+ public enum HashAlgorithm {
+ RESERVED(0, "Reserved"),
+ SHA1(1, "SHA-1"),
+ ;
+
+ HashAlgorithm(int value, String description) {
+ if (value < 0 || value > 255) {
+ throw new IllegalArgumentException();
+ }
+ this.value = (byte) value;
+ this.description = description;
+ HASH_ALGORITHM_LUT.put(this.value, this);
+ }
+
+ public final byte value;
+ public final String description;
+
+ public static HashAlgorithm forByte(byte b) {
+ return HASH_ALGORITHM_LUT.get(b);
+ }
+ }
+
+ /**
+ * The cryptographic hash algorithm used. If MiniDNS
+ * isn't aware of the hash algorithm, then this field will be
+ * null.
+ *
+ * @see #hashAlgorithmByte
+ */
+ public final HashAlgorithm hashAlgorithm;
+
+ /**
+ * The byte value of the cryptographic hash algorithm used.
+ */
+ public final byte hashAlgorithmByte;
+
+ /**
+ * Bitmap of flags: {@link #FLAG_OPT_OUT}.
+ */
+ public final byte flags;
+
+ /**
+ * The number of iterations the hash algorithm is applied.
+ */
+ public final int /* unsigned short */ iterations;
+
+ /**
+ * The salt appended to the next owner name before hashing.
+ */
+ private final byte[] salt;
+
+ /**
+ * The next hashed owner name in hash order.
+ */
+ private final byte[] nextHashed;
+
+ private final byte[] typeBitmap;
+
+ /**
+ * The RR types existing at the original owner name.
+ */
+ public final List types;
+
+ public static NSEC3 parse(DataInputStream dis, int length) throws IOException {
+ byte hashAlgorithm = dis.readByte();
+ byte flags = dis.readByte();
+ int iterations = dis.readUnsignedShort();
+ int saltLength = dis.readUnsignedByte();
+ byte[] salt = new byte[saltLength];
+ if (dis.read(salt) != salt.length) throw new IOException();
+ int hashLength = dis.readUnsignedByte();
+ byte[] nextHashed = new byte[hashLength];
+ if (dis.read(nextHashed) != nextHashed.length) throw new IOException();
+ byte[] typeBitmap = new byte[length - (6 + saltLength + hashLength)];
+ if (dis.read(typeBitmap) != typeBitmap.length) throw new IOException();
+ List types = NSEC.readTypeBitMap(typeBitmap);
+ return new NSEC3(hashAlgorithm, flags, iterations, salt, nextHashed, types);
+ }
+
+ private NSEC3(HashAlgorithm hashAlgorithm, byte hashAlgorithmByte, byte flags, int iterations, byte[] salt, byte[] nextHashed, List types) {
+ assert hashAlgorithmByte == (hashAlgorithm != null ? hashAlgorithm.value : hashAlgorithmByte);
+ this.hashAlgorithmByte = hashAlgorithmByte;
+ this.hashAlgorithm = hashAlgorithm != null ? hashAlgorithm : HashAlgorithm.forByte(hashAlgorithmByte);
+
+ this.flags = flags;
+ this.iterations = iterations;
+ this.salt = salt;
+ this.nextHashed = nextHashed;
+ this.types = types;
+ this.typeBitmap = NSEC.createTypeBitMap(types);
+ }
+
+ public NSEC3(byte hashAlgorithm, byte flags, int iterations, byte[] salt, byte[] nextHashed, List types) {
+ this(null, hashAlgorithm, flags, iterations, salt, nextHashed, types);
+ }
+
+ public NSEC3(byte hashAlgorithm, byte flags, int iterations, byte[] salt, byte[] nextHashed, TYPE... types) {
+ this(null, hashAlgorithm, flags, iterations, salt, nextHashed, Arrays.asList(types));
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.NSEC3;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeByte(hashAlgorithmByte);
+ dos.writeByte(flags);
+ dos.writeShort(iterations);
+ dos.writeByte(salt.length);
+ dos.write(salt);
+ dos.writeByte(nextHashed.length);
+ dos.write(nextHashed);
+ dos.write(typeBitmap);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(hashAlgorithm).append(' ')
+ .append(flags).append(' ')
+ .append(iterations).append(' ')
+ .append(salt.length == 0 ? "-" : new BigInteger(1, salt).toString(16).toUpperCase()).append(' ')
+ .append(Base32.encodeToString(nextHashed));
+ for (TYPE type : types) {
+ sb.append(' ').append(type);
+ }
+ return sb.toString();
+ }
+
+ public byte[] getSalt() {
+ return salt.clone();
+ }
+
+ public int getSaltLength() {
+ return salt.length;
+ }
+
+ public byte[] getNextHashed() {
+ return nextHashed.clone();
+ }
+
+ private String nextHashedBase32Cache;
+
+ public String getNextHashedBase32() {
+ if (nextHashedBase32Cache == null) {
+ nextHashedBase32Cache = Base32.encodeToString(nextHashed);
+ }
+ return nextHashedBase32Cache;
+ }
+
+ private DnsLabel nextHashedDnsLabelCache;
+
+ public DnsLabel getNextHashedDnsLabel() {
+ if (nextHashedDnsLabelCache == null) {
+ String nextHashedBase32 = getNextHashedBase32();
+ nextHashedDnsLabelCache = DnsLabel.from(nextHashedBase32);
+ }
+ return nextHashedDnsLabelCache;
+ }
+
+ public void copySaltInto(byte[] dest, int destPos) {
+ System.arraycopy(salt, 0, dest, destPos, salt.length);
+ }
+}
diff --git a/core/java/src/org/minidns/record/NSEC3PARAM.java b/core/java/src/org/minidns/record/NSEC3PARAM.java
new file mode 100644
index 000000000..0b683ef27
--- /dev/null
+++ b/core/java/src/org/minidns/record/NSEC3PARAM.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.record.NSEC3.HashAlgorithm;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+
+/**
+ * NSEC3PARAM record payload.
+ */
+public class NSEC3PARAM extends Data {
+
+ /**
+ * The cryptographic hash algorithm used.
+ *
+ */
+ public final HashAlgorithm hashAlgorithm;
+
+ /**
+ * The cryptographic hash algorithm used.
+ *
+ */
+ public final byte hashAlgorithmByte;
+
+ public final byte flags;
+
+ /**
+ * The number of iterations the hash algorithm is applied.
+ */
+ public final int /* unsigned short */ iterations;
+
+ /**
+ * The salt appended to the next owner name before hashing.
+ */
+ private final byte[] salt;
+
+ public static NSEC3PARAM parse(DataInputStream dis) throws IOException {
+ byte hashAlgorithm = dis.readByte();
+ byte flags = dis.readByte();
+ int iterations = dis.readUnsignedShort();
+ int saltLength = dis.readUnsignedByte();
+ byte[] salt = new byte[saltLength];
+ if (dis.read(salt) != salt.length && salt.length != 0) throw new IOException();
+ return new NSEC3PARAM(hashAlgorithm, flags, iterations, salt);
+ }
+
+ private NSEC3PARAM(HashAlgorithm hashAlgorithm, byte hashAlgorithmByte, byte flags, int iterations, byte[] salt) {
+ assert hashAlgorithmByte == (hashAlgorithm != null ? hashAlgorithm.value : hashAlgorithmByte);
+ this.hashAlgorithmByte = hashAlgorithmByte;
+ this.hashAlgorithm = hashAlgorithm != null ? hashAlgorithm : HashAlgorithm.forByte(hashAlgorithmByte);
+
+ this.flags = flags;
+ this.iterations = iterations;
+ this.salt = salt;
+ }
+
+ NSEC3PARAM(byte hashAlgorithm, byte flags, int iterations, byte[] salt) {
+ this(null, hashAlgorithm, flags, iterations, salt);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.NSEC3PARAM;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeByte(hashAlgorithmByte);
+ dos.writeByte(flags);
+ dos.writeShort(iterations);
+ dos.writeByte(salt.length);
+ dos.write(salt);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(hashAlgorithm).append(' ')
+ .append(flags).append(' ')
+ .append(iterations).append(' ')
+ .append(salt.length == 0 ? "-" : new BigInteger(1, salt).toString(16).toUpperCase());
+ return sb.toString();
+ }
+
+ public int getSaltLength() {
+ return salt.length;
+ }
+}
diff --git a/core/java/src/org/minidns/record/OPENPGPKEY.java b/core/java/src/org/minidns/record/OPENPGPKEY.java
new file mode 100644
index 000000000..18d9b6dd8
--- /dev/null
+++ b/core/java/src/org/minidns/record/OPENPGPKEY.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.util.Base64;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+public class OPENPGPKEY extends Data {
+
+ private final byte[] publicKeyPacket;
+
+ public static OPENPGPKEY parse(DataInputStream dis, int length) throws IOException {
+ byte[] publicKeyPacket = new byte[length];
+ dis.readFully(publicKeyPacket);
+ return new OPENPGPKEY(publicKeyPacket);
+ }
+
+ OPENPGPKEY(byte[] publicKeyPacket) {
+ this.publicKeyPacket = publicKeyPacket;
+ }
+
+ @Override
+ public Record.TYPE getType() {
+ return Record.TYPE.OPENPGPKEY;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.write(publicKeyPacket);
+ }
+
+ @Override
+ public String toString() {
+ return getPublicKeyPacketBase64();
+ }
+
+ private transient String publicKeyPacketBase64Cache;
+
+ public String getPublicKeyPacketBase64() {
+ if (publicKeyPacketBase64Cache == null) {
+ publicKeyPacketBase64Cache = Base64.encodeToString(publicKeyPacket);
+ }
+ return publicKeyPacketBase64Cache;
+ }
+
+ public byte[] getPublicKeyPacket() {
+ return publicKeyPacket.clone();
+ }
+}
diff --git a/core/java/src/org/minidns/record/OPT.java b/core/java/src/org/minidns/record/OPT.java
new file mode 100644
index 000000000..8db084777
--- /dev/null
+++ b/core/java/src/org/minidns/record/OPT.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.edns.EdnsOption;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * OPT payload (see RFC 2671 for details).
+ */
+public class OPT extends Data {
+
+ public final List variablePart;
+
+ public OPT() {
+ this(Collections.emptyList());
+ }
+
+ public OPT(List variablePart) {
+ this.variablePart = Collections.unmodifiableList(variablePart);
+ }
+
+ public static OPT parse(DataInputStream dis, int payloadLength) throws IOException {
+ List variablePart;
+ if (payloadLength == 0) {
+ variablePart = Collections.emptyList();
+ } else {
+ int payloadLeft = payloadLength;
+ variablePart = new ArrayList<>(4);
+ while (payloadLeft > 0) {
+ int optionCode = dis.readUnsignedShort();
+ int optionLength = dis.readUnsignedShort();
+ byte[] optionData = new byte[optionLength];
+ dis.read(optionData);
+ EdnsOption ednsOption = EdnsOption.parse(optionCode, optionData);
+ variablePart.add(ednsOption);
+ payloadLeft -= 2 + 2 + optionLength;
+ // Assert that payloadLeft never becomes negative
+ assert payloadLeft >= 0;
+ }
+ }
+ return new OPT(variablePart);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.OPT;
+ }
+
+ @Override
+ protected void serialize(DataOutputStream dos) throws IOException {
+ for (EdnsOption endsOption : variablePart) {
+ endsOption.writeToDos(dos);
+ }
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/PTR.java b/core/java/src/org/minidns/record/PTR.java
new file mode 100644
index 000000000..7528a1d26
--- /dev/null
+++ b/core/java/src/org/minidns/record/PTR.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * A PTR record is handled like a CNAME.
+ */
+public class PTR extends RRWithTarget {
+
+ public static PTR parse(DataInputStream dis, byte[] data) throws IOException {
+ DnsName target = DnsName.parse(dis, data);
+ return new PTR(target);
+ }
+
+ PTR(String name) {
+ this(DnsName.from(name));
+ }
+
+ PTR(DnsName name) {
+ super(name);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.PTR;
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/RRSIG.java b/core/java/src/org/minidns/record/RRSIG.java
new file mode 100644
index 000000000..05565e64e
--- /dev/null
+++ b/core/java/src/org/minidns/record/RRSIG.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+import org.minidns.util.Base64;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * RRSIG record payload.
+ */
+public class RRSIG extends Data {
+
+ /**
+ * The type of RRset covered by this signature.
+ */
+ public final TYPE typeCovered;
+
+ /**
+ * The cryptographic algorithm used to create the signature.
+ */
+ public final SignatureAlgorithm algorithm;
+
+ /**
+ * The cryptographic algorithm used to create the signature.
+ */
+ public final byte algorithmByte;
+
+ /**
+ * The number of labels in the original RRSIG RR owner name.
+ */
+ public final byte labels;
+
+ /**
+ * The TTL of the covered RRset.
+ */
+ public final long /* unsigned int */ originalTtl;
+
+ /**
+ * The date and time this RRSIG records expires.
+ */
+ public final Date signatureExpiration;
+
+ /**
+ * The date and time this RRSIG records starts to be valid.
+ */
+ public final Date signatureInception;
+
+ /**
+ * The key tag value of the DNSKEY RR that validates this signature.
+ */
+ public final int /* unsigned short */ keyTag;
+
+ /**
+ * The owner name of the DNSKEY RR that a validator is supposed to use.
+ */
+ public final DnsName signerName;
+
+ /**
+ * Signature that covers RRSIG RDATA (excluding the signature field) and RRset data.
+ */
+ private final byte[] signature;
+
+ public static RRSIG parse(DataInputStream dis, byte[] data, int length) throws IOException {
+ TYPE typeCovered = TYPE.getType(dis.readUnsignedShort());
+ byte algorithm = dis.readByte();
+ byte labels = dis.readByte();
+ long originalTtl = dis.readInt() & 0xFFFFFFFFL;
+ Date signatureExpiration = new Date((dis.readInt() & 0xFFFFFFFFL) * 1000);
+ Date signatureInception = new Date((dis.readInt() & 0xFFFFFFFFL) * 1000);
+ int keyTag = dis.readUnsignedShort();
+ DnsName signerName = DnsName.parse(dis, data);
+ int sigSize = length - signerName.size() - 18;
+ byte[] signature = new byte[sigSize];
+ if (dis.read(signature) != signature.length) throw new IOException();
+ return new RRSIG(typeCovered, null, algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName,
+ signature);
+ }
+
+ private RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte algorithmByte, byte labels, long originalTtl, Date signatureExpiration,
+ Date signatureInception, int keyTag, DnsName signerName, byte[] signature) {
+ this.typeCovered = typeCovered;
+
+ assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte);
+ this.algorithmByte = algorithmByte;
+ this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte);
+
+ this.labels = labels;
+ this.originalTtl = originalTtl;
+ this.signatureExpiration = signatureExpiration;
+ this.signatureInception = signatureInception;
+ this.keyTag = keyTag;
+ this.signerName = signerName;
+ this.signature = signature;
+ }
+
+ public RRSIG(TYPE typeCovered, int algorithm, byte labels, long originalTtl, Date signatureExpiration,
+ Date signatureInception, int keyTag, DnsName signerName, byte[] signature) {
+ this(typeCovered, null, (byte) algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName, signature);
+ }
+
+ public RRSIG(TYPE typeCovered, int algorithm, byte labels, long originalTtl, Date signatureExpiration,
+ Date signatureInception, int keyTag, String signerName, byte[] signature) {
+ this(typeCovered, null, (byte) algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, DnsName.from(signerName), signature);
+ }
+
+ public RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte labels,
+ long originalTtl, Date signatureExpiration, Date signatureInception,
+ int keyTag, DnsName signerName, byte[] signature) {
+ this(typeCovered, algorithm.number, labels, originalTtl, signatureExpiration, signatureInception,
+ keyTag, signerName, signature);
+ }
+
+ public RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte labels,
+ long originalTtl, Date signatureExpiration, Date signatureInception,
+ int keyTag, String signerName, byte[] signature) {
+ this(typeCovered, algorithm.number, labels, originalTtl, signatureExpiration, signatureInception,
+ keyTag, DnsName.from(signerName), signature);
+ }
+
+ public byte[] getSignature() {
+ return signature.clone();
+ }
+
+ public DataInputStream getSignatureAsDataInputStream() {
+ return new DataInputStream(new ByteArrayInputStream(signature));
+ }
+
+ public int getSignatureLength() {
+ return signature.length;
+ }
+
+ private transient String base64SignatureCache;
+
+ public String getSignatureBase64() {
+ if (base64SignatureCache == null) {
+ base64SignatureCache = Base64.encodeToString(signature);
+ }
+ return base64SignatureCache;
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.RRSIG;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ writePartialSignature(dos);
+ dos.write(signature);
+ }
+
+ public void writePartialSignature(DataOutputStream dos) throws IOException {
+ dos.writeShort(typeCovered.getValue());
+ dos.writeByte(algorithmByte);
+ dos.writeByte(labels);
+ dos.writeInt((int) originalTtl);
+ dos.writeInt((int) (signatureExpiration.getTime() / 1000));
+ dos.writeInt((int) (signatureInception.getTime() / 1000));
+ dos.writeShort(keyTag);
+ signerName.writeToStream(dos);
+ }
+
+ @Override
+ public String toString() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ StringBuilder sb = new StringBuilder()
+ .append(typeCovered).append(' ')
+ .append(algorithm).append(' ')
+ .append(labels).append(' ')
+ .append(originalTtl).append(' ')
+ .append(dateFormat.format(signatureExpiration)).append(' ')
+ .append(dateFormat.format(signatureInception)).append(' ')
+ .append(keyTag).append(' ')
+ .append(signerName).append(". ")
+ .append(getSignatureBase64());
+ return sb.toString();
+ }
+}
diff --git a/core/java/src/org/minidns/record/RRWithTarget.java b/core/java/src/org/minidns/record/RRWithTarget.java
new file mode 100644
index 000000000..0cc3ea77b
--- /dev/null
+++ b/core/java/src/org/minidns/record/RRWithTarget.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.minidns.dnsname.DnsName;
+
+/**
+ * A resource record pointing to a target.
+ */
+public abstract class RRWithTarget extends Data {
+
+ public final DnsName target;
+
+ /**
+ * The target of this resource record.
+ * @deprecated {@link #target} instead.
+ */
+ @Deprecated
+ public final DnsName name;
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ target.writeToStream(dos);
+ }
+
+ protected RRWithTarget(DnsName target) {
+ this.target = target;
+ this.name = target;
+ }
+
+ @Override
+ public String toString() {
+ return target + ".";
+ }
+
+ public final DnsName getTarget() {
+ return target;
+ }
+}
diff --git a/core/java/src/org/minidns/record/Record.java b/core/java/src/org/minidns/record/Record.java
new file mode 100644
index 000000000..359238feb
--- /dev/null
+++ b/core/java/src/org/minidns/record/Record.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.minidns.dnsmessage.DnsMessage;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsname.DnsName;
+
+/**
+ * A generic DNS record.
+ */
+public final class Record {
+
+ /**
+ * The resource record type.
+ *
+ * @see
+ * IANA DNS Parameters - Resource Record (RR) TYPEs
+ */
+ public enum TYPE {
+ UNKNOWN(-1),
+ A(1, A.class),
+ NS(2, NS.class),
+ MD(3),
+ MF(4),
+ CNAME(5, CNAME.class),
+ SOA(6, SOA.class),
+ MB(7),
+ MG(8),
+ MR(9),
+ NULL(10),
+ WKS(11),
+ PTR(12, PTR.class),
+ HINFO(13),
+ MINFO(14),
+ MX(15, MX.class),
+ TXT(16, TXT.class),
+ 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, AAAA.class),
+ LOC(29),
+ NXT(30),
+ EID(31),
+ NIMLOC(32),
+ SRV(33, SRV.class),
+ ATMA(34),
+ NAPTR(35),
+ KX(36),
+ CERT(37),
+ A6(38),
+ DNAME(39, DNAME.class),
+ SINK(40),
+ OPT(41, OPT.class),
+ APL(42),
+ DS(43, DS.class),
+ SSHFP(44),
+ IPSECKEY(45),
+ RRSIG(46, RRSIG.class),
+ NSEC(47, NSEC.class),
+ DNSKEY(48, DNSKEY.class),
+ DHCID(49),
+ NSEC3(50, NSEC3.class),
+ NSEC3PARAM(51, NSEC3PARAM.class),
+ TLSA(52, TLSA.class),
+ HIP(55),
+ NINFO(56),
+ RKEY(57),
+ TALINK(58),
+ CDS(59),
+ CDNSKEY(60),
+ OPENPGPKEY(61, OPENPGPKEY.class),
+ CSYNC(62),
+ SPF(99),
+ UINFO(100),
+ UID(101),
+ GID(102),
+ UNSPEC(103),
+ NID(104),
+ L32(105),
+ L64(106),
+ LP(107),
+ EUI48(108),
+ EUI64(109),
+ TKEY(249),
+ TSIG(250),
+ IXFR(251),
+ AXFR(252),
+ MAILB(253),
+ MAILA(254),
+ ANY(255),
+ URI(256),
+ CAA(257),
+ TA(32768),
+ DLV(32769, DLV.class),
+ ;
+
+ /**
+ * The value of this DNS record type.
+ */
+ private final int value;
+
+ private final Class> dataClass;
+
+ /**
+ * Internal lookup table to map values to types.
+ */
+ private static final Map INVERSE_LUT = new HashMap<>();
+
+ private static final Map, TYPE> DATA_LUT = new HashMap<>();
+
+ static {
+ // Initialize the reverse lookup table.
+ for (TYPE t : TYPE.values()) {
+ INVERSE_LUT.put(t.getValue(), t);
+ if (t.dataClass != null) {
+ DATA_LUT.put(t.dataClass, t);
+ }
+ }
+ }
+
+ /**
+ * Create a new record type.
+ *
+ * @param value The binary value of this type.
+ */
+ TYPE(int value) {
+ this(value, null);
+ }
+
+ /**
+ * Create a new record type.
+ *
+ * @param The class for this type.
+ * @param dataClass The class for this type.
+ * @param value The binary value of this type.
+ */
+ TYPE(int value, Class dataClass) {
+ this.value = value;
+ this.dataClass = dataClass;
+ }
+
+ /**
+ * Retrieve the binary value of this type.
+ * @return The binary value.
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Get the {@link Data} class for this type.
+ *
+ * @param The class for this type.
+ * @return the {@link Data} class for this type.
+ */
+ @SuppressWarnings("unchecked")
+ public Class getDataClass() {
+ return (Class) dataClass;
+ }
+
+ /**
+ * Retrieve the symbolic type of the binary value.
+ * @param value The binary type value.
+ * @return The symbolic tpye.
+ */
+ public static TYPE getType(int value) {
+ TYPE type = INVERSE_LUT.get(value);
+ if (type == null) return UNKNOWN;
+ return type;
+ }
+
+ /**
+ * Retrieve the type for a given {@link Data} class.
+ *
+ * @param The class for this type.
+ * @param dataClass the class to lookup the type for.
+ * @return the type for the given data class.
+ */
+ public static TYPE getType(Class dataClass) {
+ return DATA_LUT.get(dataClass);
+ }
+ }
+
+ /**
+ * The symbolic class of a DNS record (usually {@link CLASS#IN} for Internet).
+ *
+ * @see IANA Domain Name System (DNS) Parameters - DNS CLASSes
+ */
+ public enum CLASS {
+
+ /**
+ * The Internet class. This is the most common class used by todays DNS systems.
+ */
+ IN(1),
+
+ /**
+ * The Chaos class.
+ */
+ CH(3),
+
+ /**
+ * The Hesiod class.
+ */
+ HS(4),
+ NONE(254),
+ ANY(255);
+
+ /**
+ * Internal reverse lookup table to map binary class values to symbolic
+ * names.
+ */
+ private static final HashMap INVERSE_LUT =
+ new HashMap();
+
+ static {
+ // Initialize the interal reverse lookup table.
+ 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.
+ */
+ 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.
+ */
+ public final DnsName name;
+
+ /**
+ * The type (and payload type) of this record.
+ */
+ public final TYPE type;
+
+ /**
+ * The record class (usually CLASS.IN).
+ */
+ public final CLASS clazz;
+
+ /**
+ * The value of the class field of a RR.
+ *
+ * According to RFC 2671 (OPT RR) this is not necessarily representable
+ * using clazz field and unicastQuery bit
+ */
+ public final int clazzValue;
+
+ /**
+ * The ttl of this record.
+ */
+ public final long ttl;
+
+ /**
+ * The payload object of this record.
+ */
+ public final D payloadData;
+
+ /**
+ * MDNS defines the highest bit of the class as the unicast query bit.
+ */
+ public final boolean unicastQuery;
+
+ /**
+ * 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.
+ * @return the record which was parsed.
+ * @throws IOException In case of malformed replies.
+ */
+ public static Record parse(DataInputStream dis, byte[] data) throws IOException {
+ DnsName name = DnsName.parse(dis, data);
+ int typeValue = dis.readUnsignedShort();
+ TYPE type = TYPE.getType(typeValue);
+ int clazzValue = dis.readUnsignedShort();
+ CLASS clazz = CLASS.getClass(clazzValue & 0x7fff);
+ boolean unicastQuery = (clazzValue & 0x8000) > 0;
+ long ttl = (((long) dis.readUnsignedShort()) << 16) +
+ dis.readUnsignedShort();
+ int payloadLength = dis.readUnsignedShort();
+ Data payloadData;
+ switch (type) {
+ case SOA:
+ payloadData = SOA.parse(dis, data);
+ break;
+ case SRV:
+ payloadData = SRV.parse(dis, data);
+ break;
+ case MX:
+ payloadData = MX.parse(dis, data);
+ break;
+ case AAAA:
+ payloadData = AAAA.parse(dis);
+ break;
+ case A:
+ payloadData = A.parse(dis);
+ break;
+ case NS:
+ payloadData = NS.parse(dis, data);
+ break;
+ case CNAME:
+ payloadData = CNAME.parse(dis, data);
+ break;
+ case DNAME:
+ payloadData = DNAME.parse(dis, data);
+ break;
+ case PTR:
+ payloadData = PTR.parse(dis, data);
+ break;
+ case TXT:
+ payloadData = TXT.parse(dis, payloadLength);
+ break;
+ case OPT:
+ payloadData = OPT.parse(dis, payloadLength);
+ break;
+ case DNSKEY:
+ payloadData = DNSKEY.parse(dis, payloadLength);
+ break;
+ case RRSIG:
+ payloadData = RRSIG.parse(dis, data, payloadLength);
+ break;
+ case DS:
+ payloadData = DS.parse(dis, payloadLength);
+ break;
+ case NSEC:
+ payloadData = NSEC.parse(dis, data, payloadLength);
+ break;
+ case NSEC3:
+ payloadData = NSEC3.parse(dis, payloadLength);
+ break;
+ case NSEC3PARAM:
+ payloadData = NSEC3PARAM.parse(dis);
+ break;
+ case TLSA:
+ payloadData = TLSA.parse(dis, payloadLength);
+ break;
+ case OPENPGPKEY:
+ payloadData = OPENPGPKEY.parse(dis, payloadLength);
+ break;
+ case DLV:
+ payloadData = DLV.parse(dis, payloadLength);
+ break;
+ case UNKNOWN:
+ default:
+ payloadData = UNKNOWN.parse(dis, payloadLength, type);
+ break;
+ }
+ return new Record<>(name, type, clazz, clazzValue, ttl, payloadData, unicastQuery);
+ }
+
+ public Record(DnsName name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) {
+ this(name, type, clazz, clazz.getValue() + (unicastQuery ? 0x8000 : 0), ttl, payloadData, unicastQuery);
+ }
+
+ public Record(String name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) {
+ this(DnsName.from(name), type, clazz, ttl, payloadData, unicastQuery);
+ }
+
+ public Record(String name, TYPE type, int clazzValue, long ttl, D payloadData) {
+ this(DnsName.from(name), type, CLASS.NONE, clazzValue, ttl, payloadData, false);
+ }
+
+ public Record(DnsName name, TYPE type, int clazzValue, long ttl, D payloadData) {
+ this(name, type, CLASS.NONE, clazzValue, ttl, payloadData, false);
+ }
+
+ private Record(DnsName name, TYPE type, CLASS clazz, int clazzValue, long ttl, D payloadData, boolean unicastQuery) {
+ this.name = name;
+ this.type = type;
+ this.clazz = clazz;
+ this.clazzValue = clazzValue;
+ this.ttl = ttl;
+ this.payloadData = payloadData;
+ this.unicastQuery = unicastQuery;
+ }
+
+ public void toOutputStream(OutputStream outputStream) throws IOException {
+ if (payloadData == null) {
+ throw new IllegalStateException("Empty Record has no byte representation");
+ }
+
+ DataOutputStream dos = new DataOutputStream(outputStream);
+
+ name.writeToStream(dos);
+ dos.writeShort(type.getValue());
+ dos.writeShort(clazzValue);
+ dos.writeInt((int) ttl);
+
+ dos.writeShort(payloadData.length());
+ payloadData.toOutputStream(dos);
+ }
+
+ private transient byte[] bytes;
+
+ public byte[] toByteArray() {
+ if (bytes == null) {
+ int totalSize = name.size()
+ + 10 // 2 byte short type + 2 byte short classValue + 4 byte int ttl + 2 byte short payload length.
+ + payloadData.length();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(totalSize);
+ DataOutputStream dos = new DataOutputStream(baos);
+ try {
+ toOutputStream(dos);
+ } catch (IOException e) {
+ // Should never happen.
+ throw new AssertionError(e);
+ }
+ bytes = baos.toByteArray();
+ }
+ return bytes.clone();
+ }
+
+ /**
+ * Retrieve a textual representation of this resource record.
+ * @return String
+ */
+ @Override
+ public String toString() {
+ return name.getRawAce() + ".\t" + ttl + '\t' + clazz + '\t' + type + '\t' + payloadData;
+ }
+
+ /**
+ * 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.type == type) || (q.type == TYPE.ANY)) &&
+ ((q.clazz == clazz) || (q.clazz == CLASS.ANY)) &&
+ q.name.equals(name);
+ }
+
+ /**
+ * See if this query/response was a unicast query (highest class bit set).
+ * @return True if it is a unicast query/response record.
+ */
+ public boolean isUnicastQuery() {
+ return unicastQuery;
+ }
+
+ /**
+ * The payload data, usually a subclass of data (A, AAAA, CNAME, ...).
+ * @return The payload data.
+ */
+ public D getPayload() {
+ return payloadData;
+ }
+
+ /**
+ * Retrieve the record ttl.
+ * @return The record ttl.
+ */
+ public long getTtl() {
+ return ttl;
+ }
+
+ /**
+ * Get the question asking for this resource record. This will return null if the record is not retrievable, i.e.
+ * {@link TYPE#OPT}.
+ *
+ * @return the question for this resource record or null.
+ */
+ public Question getQuestion() {
+ switch (type) {
+ case OPT:
+ // OPT records are not retrievable.
+ return null;
+ case RRSIG:
+ RRSIG rrsig = (RRSIG) payloadData;
+ return new Question(name, rrsig.typeCovered, clazz);
+ default:
+ return new Question(name, type, clazz);
+ }
+ }
+
+ public DnsMessage.Builder getQuestionMessage() {
+ Question question = getQuestion();
+ if (question == null) {
+ return null;
+ }
+ return question.asMessageBuilder();
+ }
+
+ private transient Integer hashCodeCache;
+
+ @Override
+ public int hashCode() {
+ if (hashCodeCache == null) {
+ int hashCode = 1;
+ hashCode = 37 * hashCode + name.hashCode();
+ hashCode = 37 * hashCode + type.hashCode();
+ hashCode = 37 * hashCode + clazz.hashCode();
+ hashCode = 37 * hashCode + payloadData.hashCode();
+ hashCodeCache = hashCode;
+ }
+ return hashCodeCache;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Record)) {
+ return false;
+ }
+ if (other == this) {
+ return true;
+ }
+ Record> otherRecord = (Record>) other;
+ if (!name.equals(otherRecord.name)) return false;
+ if (type != otherRecord.type) return false;
+ if (clazz != otherRecord.clazz) return false;
+ // Note that we do not compare the TTL here, since we consider two Records with everything but the TTL equal to
+ // be equal too.
+ if (!payloadData.equals(otherRecord.payloadData)) return false;
+
+ return true;
+ }
+
+ /**
+ * Return the record if possible as record with the given {@link Data} class. If the record does not hold payload of
+ * the given data class type, then {@code null} will be returned.
+ *
+ * @param dataClass a class of the {@link Data} type.
+ * @param a subtype of {@link Data}.
+ * @return the record with a specialized payload type or {@code null}.
+ * @see #as(Class)
+ */
+ @SuppressWarnings("unchecked")
+ public Record ifPossibleAs(Class dataClass) {
+ if (type.dataClass == dataClass) {
+ return (Record) this;
+ }
+ return null;
+ }
+
+ /**
+ * Return the record as record with the given {@link Data} class. If the record does not hold payload of
+ * the given data class type, then a {@link IllegalArgumentException} will be thrown.
+ *
+ * @param dataClass a class of the {@link Data} type.
+ * @param a subtype of {@link Data}.
+ * @return the record with a specialized payload type.
+ * @see #ifPossibleAs(Class)
+ */
+ public Record as(Class dataClass) {
+ Record eRecord = ifPossibleAs(dataClass);
+ if (eRecord == null) {
+ throw new IllegalArgumentException("The instance " + this + " can not be cast to a Record with" + dataClass);
+ }
+ return eRecord;
+ }
+
+ public static void filter(Collection> result, Class dataClass,
+ Collection> input) {
+ for (Record extends Data> record : input) {
+ Record filteredRecord = record.ifPossibleAs(dataClass);
+ if (filteredRecord == null)
+ continue;
+
+ result.add(filteredRecord);
+ }
+ }
+
+ public static List> filter(Class dataClass,
+ Collection> input) {
+ List> result = new ArrayList<>(input.size());
+ filter(result, dataClass, input);
+ return result;
+ }
+}
diff --git a/core/java/src/org/minidns/record/SOA.java b/core/java/src/org/minidns/record/SOA.java
new file mode 100644
index 000000000..62b9f6d9a
--- /dev/null
+++ b/core/java/src/org/minidns/record/SOA.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * SOA (start of authority) record payload.
+ */
+public class SOA extends Data {
+
+ /**
+ * The domain name of the name server that was the original or primary source of data for this zone.
+ */
+ public final DnsName mname;
+
+ /**
+ * A domain name which specifies the mailbox of the person responsible for this zone.
+ */
+ public final DnsName rname;
+
+ /**
+ * The unsigned 32 bit version number of the original copy of the zone. Zone transfers preserve this value. This
+ * value wraps and should be compared using sequence space arithmetic.
+ */
+ public final long /* unsigned int */ serial;
+
+ /**
+ * A 32 bit time interval before the zone should be refreshed.
+ */
+ public final int refresh;
+
+ /**
+ * A 32 bit time interval that should elapse before a failed refresh should be retried.
+ */
+ public final int retry;
+
+ /**
+ * A 32 bit time value that specifies the upper limit on the time interval that can elapse before the zone is no
+ * longer authoritative.
+ */
+ public final int expire;
+
+ /**
+ * The unsigned 32 bit minimum TTL field that should be exported with any RR from this zone.
+ */
+ public final long /* unsigned int */ minimum;
+
+ public static SOA parse(DataInputStream dis, byte[] data)
+ throws IOException {
+ DnsName mname = DnsName.parse(dis, data);
+ DnsName rname = DnsName.parse(dis, data);
+ long serial = dis.readInt() & 0xFFFFFFFFL;
+ int refresh = dis.readInt();
+ int retry = dis.readInt();
+ int expire = dis.readInt();
+ long minimum = dis.readInt() & 0xFFFFFFFFL;
+ return new SOA(mname, rname, serial, refresh, retry, expire, minimum);
+ }
+
+ public SOA(String mname, String rname, long serial, int refresh, int retry, int expire, long minimum) {
+ this(DnsName.from(mname), DnsName.from(rname), serial, refresh, retry, expire, minimum);
+ }
+
+ public SOA(DnsName mname, DnsName rname, long serial, int refresh, int retry, int expire, long minimum) {
+ this.mname = mname;
+ this.rname = rname;
+ this.serial = serial;
+ this.refresh = refresh;
+ this.retry = retry;
+ this.expire = expire;
+ this.minimum = minimum;
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.SOA;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ mname.writeToStream(dos);
+ rname.writeToStream(dos);
+ dos.writeInt((int) serial);
+ dos.writeInt(refresh);
+ dos.writeInt(retry);
+ dos.writeInt(expire);
+ dos.writeInt((int) minimum);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder()
+ .append(mname).append(". ")
+ .append(rname).append(". ")
+ .append(serial).append(' ')
+ .append(refresh).append(' ')
+ .append(retry).append(' ')
+ .append(expire).append(' ')
+ .append(minimum);
+ return sb.toString();
+ }
+}
diff --git a/core/java/src/org/minidns/record/SRV.java b/core/java/src/org/minidns/record/SRV.java
new file mode 100644
index 000000000..5d04bd9f2
--- /dev/null
+++ b/core/java/src/org/minidns/record/SRV.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.minidns.dnsname.DnsName;
+import org.minidns.record.Record.TYPE;
+
+/**
+ * SRV record payload (service pointer).
+ */
+public class SRV extends Data implements Comparable {
+
+ /**
+ * The priority of this service. Lower values mean higher priority.
+ */
+ public final int priority;
+
+ /**
+ * The weight of this service. Services with the same priority should be
+ * balanced based on weight.
+ */
+ public final int weight;
+
+ /**
+ * The target port.
+ */
+ public final int port;
+
+ /**
+ * The target server.
+ */
+ public final DnsName target;
+
+ /**
+ * The target server.
+ *
+ * @deprecated use {@link #target} instead.
+ */
+ @Deprecated
+ public final DnsName name;
+
+ public static SRV parse(DataInputStream dis, byte[] data)
+ throws IOException {
+ int priority = dis.readUnsignedShort();
+ int weight = dis.readUnsignedShort();
+ int port = dis.readUnsignedShort();
+ DnsName name = DnsName.parse(dis, data);
+ return new SRV(priority, weight, port, name);
+ }
+
+ public SRV(int priority, int weight, int port, String name) {
+ this(priority, weight, port, DnsName.from(name));
+ }
+
+ public SRV(int priority, int weight, int port, DnsName name) {
+ this.priority = priority;
+ this.weight = weight;
+ this.port = port;
+ this.target = name;
+ this.name = target;
+ }
+
+ /**
+ * Check if the service is available at this domain. This checks f the target points to the root label. As per RFC
+ * 2782 the service is decidedly not available if there is only a single SRV answer pointing to the root label. From
+ * RFC 2782:
+ *
+ * A Target of "." means that the service is decidedly not available at this domain.
+ *
+ * @return true if the service is available at this domain.
+ */
+ public boolean isServiceAvailable() {
+ return !target.isRootLabel();
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeShort(priority);
+ dos.writeShort(weight);
+ dos.writeShort(port);
+ target.writeToStream(dos);
+ }
+
+ @Override
+ public String toString() {
+ return priority + " " + weight + " " + port + " " + target + ".";
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.SRV;
+ }
+
+ @Override
+ public int compareTo(SRV other) {
+ int res = other.priority - this.priority;
+ if (res == 0) {
+ res = this.weight - other.weight;
+ }
+ return res;
+ }
+}
diff --git a/core/java/src/org/minidns/record/TLSA.java b/core/java/src/org/minidns/record/TLSA.java
new file mode 100644
index 000000000..8de8ea788
--- /dev/null
+++ b/core/java/src/org/minidns/record/TLSA.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TLSA extends Data {
+
+ private static final Map CERT_USAGE_LUT = new HashMap<>();
+
+ public enum CertUsage {
+
+ caConstraint((byte) 0),
+ serviceCertificateConstraint((byte) 1),
+ trustAnchorAssertion((byte) 2),
+ domainIssuedCertificate((byte) 3),
+ ;
+
+ public final byte byteValue;
+
+ CertUsage(byte byteValue) {
+ this.byteValue = byteValue;
+ CERT_USAGE_LUT.put(byteValue, this);
+ }
+ }
+
+ private static final Map SELECTOR_LUT = new HashMap<>();
+
+ public enum Selector {
+ fullCertificate((byte) 0),
+ subjectPublicKeyInfo((byte) 1),
+ ;
+
+ public final byte byteValue;
+
+ Selector(byte byteValue) {
+ this.byteValue = byteValue;
+ SELECTOR_LUT.put(byteValue, this);
+ }
+ }
+
+ private static final Map MATCHING_TYPE_LUT = new HashMap<>();
+
+ public enum MatchingType {
+ noHash((byte) 0),
+ sha256((byte) 1),
+ sha512((byte) 2),
+ ;
+
+ public final byte byteValue;
+
+ MatchingType(byte byteValue) {
+ this.byteValue = byteValue;
+ MATCHING_TYPE_LUT.put(byteValue, this);
+ }
+ }
+
+ static {
+ // Ensure that the LUTs are initialized.
+ CertUsage.values();
+ Selector.values();
+ MatchingType.values();
+ }
+
+ /**
+ * The provided association that will be used to match the certificate presented in
+ * the TLS handshake.
+ */
+ public final byte certUsageByte;
+
+ public final CertUsage certUsage;
+
+ /**
+ * Which part of the TLS certificate presented by the server will be matched against the
+ * association data.
+ */
+ public final byte selectorByte;
+
+ public final Selector selector;
+
+ /**
+ * How the certificate association is presented.
+ */
+ public final byte matchingTypeByte;
+
+ public final MatchingType matchingType;
+
+ /**
+ * The "certificate association data" to be matched.
+ */
+ private final byte[] certificateAssociation;
+
+ public static TLSA parse(DataInputStream dis, int length) throws IOException {
+ byte certUsage = dis.readByte();
+ byte selector = dis.readByte();
+ byte matchingType = dis.readByte();
+ byte[] certificateAssociation = new byte[length - 3];
+ if (dis.read(certificateAssociation) != certificateAssociation.length) throw new IOException();
+ return new TLSA(certUsage, selector, matchingType, certificateAssociation);
+ }
+
+ TLSA(byte certUsageByte, byte selectorByte, byte matchingTypeByte, byte[] certificateAssociation) {
+ this.certUsageByte = certUsageByte;
+ this.certUsage = CERT_USAGE_LUT.get(certUsageByte);
+
+ this.selectorByte = selectorByte;
+ this.selector = SELECTOR_LUT.get(selectorByte);
+
+ this.matchingTypeByte = matchingTypeByte;
+ this.matchingType = MATCHING_TYPE_LUT.get(matchingTypeByte);
+
+ this.certificateAssociation = certificateAssociation;
+ }
+
+ @Override
+ public Record.TYPE getType() {
+ return Record.TYPE.TLSA;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.writeByte(certUsageByte);
+ dos.writeByte(selectorByte);
+ dos.writeByte(matchingTypeByte);
+ dos.write(certificateAssociation);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append(certUsageByte).append(' ')
+ .append(selectorByte).append(' ')
+ .append(matchingTypeByte).append(' ')
+ .append(new BigInteger(1, certificateAssociation).toString(16)).toString();
+ }
+
+ public byte[] getCertificateAssociation() {
+ return certificateAssociation.clone();
+ }
+
+ public boolean certificateAssociationEquals(byte[] otherCertificateAssociation) {
+ return Arrays.equals(certificateAssociation, otherCertificateAssociation);
+ }
+}
diff --git a/core/java/src/org/minidns/record/TXT.java b/core/java/src/org/minidns/record/TXT.java
new file mode 100644
index 000000000..4e0551066
--- /dev/null
+++ b/core/java/src/org/minidns/record/TXT.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.minidns.record.Record.TYPE;
+
+/**
+ * A TXT record. Actually a binary blob containing extents, each of which is a one-byte count
+ * followed by that many bytes of data, which can usually be interpreted as ASCII strings
+ * but not always.
+ */
+public class TXT extends Data {
+
+ private final byte[] blob;
+
+ public static TXT parse(DataInputStream dis, int length) throws IOException {
+ byte[] blob = new byte[length];
+ dis.readFully(blob);
+ return new TXT(blob);
+ }
+
+ public TXT(byte[] blob) {
+ this.blob = blob;
+ }
+
+ public byte[] getBlob() {
+ return blob.clone();
+ }
+
+ private transient String textCache;
+
+ public String getText() {
+ if (textCache == null) {
+ StringBuilder sb = new StringBuilder();
+ Iterator it = getCharacterStrings().iterator();
+ while (it.hasNext()) {
+ sb.append(it.next());
+ if (it.hasNext()) {
+ sb.append(" / ");
+ }
+ }
+ textCache = sb.toString();
+ }
+ return textCache;
+ }
+
+ private transient List characterStringsCache;
+
+ public List getCharacterStrings() {
+ if (characterStringsCache == null) {
+ List extents = getExtents();
+ List characterStrings = new ArrayList<>(extents.size());
+ for (byte[] extent : extents) {
+ try {
+ characterStrings.add(new String(extent, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ characterStringsCache = Collections.unmodifiableList(characterStrings);
+ }
+ return characterStringsCache;
+ }
+
+ public List getExtents() {
+ ArrayList extents = new ArrayList();
+ int segLength = 0;
+ for (int used = 0; used < blob.length; used += segLength) {
+ segLength = 0x00ff & blob[used];
+ int end = ++used + segLength;
+ byte[] extent = Arrays.copyOfRange(blob, used, end);
+ extents.add(extent);
+ }
+ return extents;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.write(blob);
+ }
+
+ @Override
+ public TYPE getType() {
+ return TYPE.TXT;
+ }
+
+ @Override
+ public String toString() {
+ return "\"" + getText() + "\"";
+ }
+
+}
diff --git a/core/java/src/org/minidns/record/UNKNOWN.java b/core/java/src/org/minidns/record/UNKNOWN.java
new file mode 100644
index 000000000..4a13f4dc2
--- /dev/null
+++ b/core/java/src/org/minidns/record/UNKNOWN.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.record;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.minidns.record.Record.TYPE;
+
+public final class UNKNOWN extends Data {
+
+ private final TYPE type;
+ private final byte[] data;
+
+ private UNKNOWN(DataInputStream dis, int payloadLength, TYPE type) throws IOException {
+ this.type = type;
+ this.data = new byte[payloadLength];
+ dis.readFully(data);
+ }
+
+ @Override
+ public TYPE getType() {
+ return type;
+ }
+
+ @Override
+ public void serialize(DataOutputStream dos) throws IOException {
+ dos.write(data);
+ }
+
+ public static UNKNOWN parse(DataInputStream dis, int payloadLength, TYPE type)
+ throws IOException {
+ return new UNKNOWN(dis, payloadLength, type);
+ }
+
+}
diff --git a/core/java/src/org/minidns/util/Base32.java b/core/java/src/org/minidns/util/Base32.java
new file mode 100644
index 000000000..848e70636
--- /dev/null
+++ b/core/java/src/org/minidns/util/Base32.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.util;
+
+/**
+ * Very minimal Base32 encoder.
+ */
+public final class Base32 {
+ private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+ private static final String PADDING = "======";
+
+ /**
+ * Do not allow to instantiate Base32
+ */
+ private Base32() {
+ }
+
+ public static String encodeToString(byte[] bytes) {
+ int paddingCount = (int) (8 - (bytes.length % 5) * 1.6) % 8;
+ byte[] padded = new byte[bytes.length + paddingCount];
+ System.arraycopy(bytes, 0, padded, 0, bytes.length);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < bytes.length; i += 5) {
+ long j = ((long) (padded[i] & 0xff) << 32) + ((long) (padded[i + 1] & 0xff) << 24)
+ + ((padded[i + 2] & 0xff) << 16) + ((padded[i + 3] & 0xff) << 8) + (padded[i + 4] & 0xff);
+ sb.append(ALPHABET.charAt((int) ((j >> 35) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 30) & 0x1f)))
+ .append(ALPHABET.charAt((int) ((j >> 25) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 20) & 0x1f)))
+ .append(ALPHABET.charAt((int) ((j >> 15) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 10) & 0x1f)))
+ .append(ALPHABET.charAt((int) ((j >> 5) & 0x1f))).append(ALPHABET.charAt((int) (j & 0x1f)));
+ }
+ return sb.substring(0, sb.length() - paddingCount) + PADDING.substring(0, paddingCount);
+ }
+}
diff --git a/core/java/src/org/minidns/util/Base64.java b/core/java/src/org/minidns/util/Base64.java
new file mode 100644
index 000000000..aa334f3a0
--- /dev/null
+++ b/core/java/src/org/minidns/util/Base64.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.util;
+
+/**
+ * Very minimal Base64 encoder.
+ */
+public final class Base64 {
+ private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ private static final String PADDING = "==";
+
+ /**
+ * Do not allow to instantiate Base64
+ */
+ private Base64() {
+ }
+
+ public static String encodeToString(byte[] bytes) {
+ int paddingCount = (3 - (bytes.length % 3)) % 3;
+ byte[] padded = new byte[bytes.length + paddingCount];
+ System.arraycopy(bytes, 0, padded, 0, bytes.length);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < bytes.length; i += 3) {
+ int j = ((padded[i] & 0xff) << 16) + ((padded[i + 1] & 0xff) << 8) + (padded[i + 2] & 0xff);
+ sb.append(ALPHABET.charAt((j >> 18) & 0x3f)).append(ALPHABET.charAt((j >> 12) & 0x3f))
+ .append(ALPHABET.charAt((j >> 6) & 0x3f)).append(ALPHABET.charAt(j & 0x3f));
+ }
+ return sb.substring(0, sb.length() - paddingCount) + PADDING.substring(0, paddingCount);
+ }
+}
diff --git a/core/java/src/org/minidns/util/Hex.java b/core/java/src/org/minidns/util/Hex.java
new file mode 100644
index 000000000..c86fe2d4f
--- /dev/null
+++ b/core/java/src/org/minidns/util/Hex.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.util;
+
+public class Hex {
+
+ public static StringBuilder from(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (byte b : bytes) {
+ sb.append(String.format("%02X ", b));
+ }
+ return sb;
+ }
+}
diff --git a/core/java/src/org/minidns/util/InetAddressUtil.java b/core/java/src/org/minidns/util/InetAddressUtil.java
new file mode 100644
index 000000000..999c7d317
--- /dev/null
+++ b/core/java/src/org/minidns/util/InetAddressUtil.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2015-2020 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package org.minidns.util;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+import org.minidns.dnsname.DnsName;
+
+public class InetAddressUtil {
+
+ public static Inet4Address ipv4From(CharSequence cs) {
+ InetAddress inetAddress;
+ try {
+ inetAddress = InetAddress.getByName(cs.toString());
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException(e);
+ }
+ if (inetAddress instanceof Inet4Address) {
+ return (Inet4Address) inetAddress;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ public static Inet6Address ipv6From(CharSequence cs) {
+ InetAddress inetAddress;
+ try {
+ inetAddress = InetAddress.getByName(cs.toString());
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException(e);
+ }
+ if (inetAddress instanceof Inet6Address) {
+ return (Inet6Address) inetAddress;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ // IPV4_REGEX from http://stackoverflow.com/a/46168/194894 by Kevin Wong (http://stackoverflow.com/users/4792/kevin-wong) licensed under
+ // CC BY-SA 3.0.
+ private static final Pattern IPV4_PATTERN = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
+
+ public static boolean isIpV4Address(CharSequence address) {
+ if (address == null) {
+ return false;
+ }
+ return IPV4_PATTERN.matcher(address).matches();
+ }
+
+ // IPv6 Regular Expression from http://stackoverflow.com/a/17871737/194894 by David M. Syzdek
+ // (http://stackoverflow.com/users/903194/david-m-syzdek) licensed under CC BY-SA 3.0.
+ private static final Pattern IPV6_PATTERN = Pattern.compile(
+ "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))");
+
+ public static boolean isIpV6Address(CharSequence address) {
+ if (address == null) {
+ return false;
+ }
+ return IPV6_PATTERN.matcher(address).matches();
+ }
+
+ public static boolean isIpAddress(CharSequence address) {
+ return isIpV6Address(address) || isIpV4Address(address);
+ }
+
+ public static InetAddress convertToInetAddressIfPossible(CharSequence address) {
+ if (!isIpAddress(address)) {
+ return null;
+ }
+
+ String addressString = address.toString();
+ try {
+ return InetAddress.getByName(addressString);
+ } catch (UnknownHostException e) {
+ // Should never happen.
+ throw new AssertionError(e);
+ }
+ }
+
+ public static DnsName reverseIpAddressOf(Inet6Address inet6Address) {
+ final String ipAddress = inet6Address.getHostAddress();
+ final String[] ipAddressParts = ipAddress.split(":");
+
+ String[] parts = new String[32];
+ int currentPartNum = 0;
+ for (int i = ipAddressParts.length - 1; i >= 0; i--) {
+ final String currentPart = ipAddressParts[i];
+ final int missingPlaces = 4 - currentPart.length();
+ for (int j = 0; j < missingPlaces; j++) {
+ parts[currentPartNum++] = "0";
+ }
+ for (int j = 0; j < currentPart.length(); j++) {
+ parts[currentPartNum++] = Character.toString(currentPart.charAt(j));
+ }
+ }
+
+ return DnsName.from(parts);
+ }
+
+ public static DnsName reverseIpAddressOf(Inet4Address inet4Address) {
+ final String[] ipAddressParts = inet4Address.getHostAddress().split("\\.");
+ assert ipAddressParts.length == 4;
+
+ return DnsName.from(ipAddressParts);
+ }
+}