diff --git a/LICENSE.txt b/LICENSE.txt index 9d984ce28..357305988 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -94,6 +94,12 @@ Public domain except as listed below: Copyright (C) 2016 Southern Storm Software, Pty Ltd. See licenses/LICENSE-Noise.txt + MiniDNS library 1.0.0 + This software may be used under the terms of (at your choice) + - LGPL version 2 (or later) (see licenses/LICENSE-LGPL2.1.txt) + - Apache Software licence (see licenses/LICENSE-Apache2.0.txt) + - DWTFYWTPL + Router (router.jar): Public domain except as listed below: diff --git a/build.xml b/build.xml index 84651fe97..cbcce8c37 100644 --- a/build.xml +++ b/build.xml @@ -956,7 +956,7 @@ additionalparam="-notimestamp" doctitle="I2P Javadocs for Release ${release.number} Build ${i2p.build.number}${build.extra}" windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}"> - + diff --git a/core/java/src/org/minidns/constants/DnssecConstants.java b/core/java/src/org/minidns/constants/DnssecConstants.java new file mode 100644 index 000000000..7f8395ce7 --- /dev/null +++ b/core/java/src/org/minidns/constants/DnssecConstants.java @@ -0,0 +1,99 @@ +/* + * 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.constants; + +import java.util.HashMap; +import java.util.Map; + +public final class DnssecConstants { + /** + * Do not allow to instantiate DNSSECConstants + */ + private DnssecConstants() { + } + + private static final Map SIGNATURE_ALGORITHM_LUT = new HashMap<>(); + + /** + * DNSSEC Signature Algorithms. + * + * @see + * IANA DNSSEC Algorithm Numbers + */ + public enum SignatureAlgorithm { + @Deprecated + RSAMD5(1, "RSA/MD5"), + DH(2, "Diffie-Hellman"), + DSA(3, "DSA/SHA1"), + RSASHA1(5, "RSA/SHA-1"), + DSA_NSEC3_SHA1(6, "DSA_NSEC3-SHA1"), + RSASHA1_NSEC3_SHA1(7, "RSASHA1-NSEC3-SHA1"), + RSASHA256(8, "RSA/SHA-256"), + RSASHA512(10, "RSA/SHA-512"), + ECC_GOST(12, "GOST R 34.10-2001"), + ECDSAP256SHA256(13, "ECDSA Curve P-256 with SHA-256"), + ECDSAP384SHA384(14, "ECDSA Curve P-384 with SHA-384"), + INDIRECT(252, "Reserved for Indirect Keys"), + PRIVATEDNS(253, "private algorithm"), + PRIVATEOID(254, "private algorithm oid"), + ; + + SignatureAlgorithm(int number, String description) { + if (number < 0 || number > 255) { + throw new IllegalArgumentException(); + } + this.number = (byte) number; + this.description = description; + SIGNATURE_ALGORITHM_LUT.put(this.number, this); + } + + public final byte number; + public final String description; + + public static SignatureAlgorithm forByte(byte b) { + return SIGNATURE_ALGORITHM_LUT.get(b); + } + } + + private static final Map DELEGATION_DIGEST_LUT = new HashMap<>(); + + /** + * DNSSEC Digest Algorithms. + * + * @see + * IANA Delegation Signer (DS) Resource Record (RR) + */ + public enum DigestAlgorithm { + SHA1(1, "SHA-1"), + SHA256(2, "SHA-256"), + GOST(3, "GOST R 34.11-94"), + SHA384(4, "SHA-384"), + ; + + DigestAlgorithm(int value, String description) { + if (value < 0 || value > 255) { + throw new IllegalArgumentException(); + } + this.value = (byte) value; + this.description = description; + DELEGATION_DIGEST_LUT.put(this.value, this); + } + + public final byte value; + public final String description; + + public static DigestAlgorithm forByte(byte b) { + return DELEGATION_DIGEST_LUT.get(b); + } + } +} diff --git a/core/java/src/org/minidns/dnslabel/ALabel.java b/core/java/src/org/minidns/dnslabel/ALabel.java new file mode 100644 index 000000000..ee3ba1bb9 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/ALabel.java @@ -0,0 +1,25 @@ +/* + * 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.dnslabel; + +import org.minidns.idna.MiniDnsIdna; + +public final class ALabel extends XnLabel { + + protected ALabel(String label) { + super(label); + } + + @Override + protected String getInternationalizedRepresentationInternal() { + return MiniDnsIdna.toUnicode(label); + } +} diff --git a/core/java/src/org/minidns/dnslabel/DnsLabel.java b/core/java/src/org/minidns/dnslabel/DnsLabel.java new file mode 100644 index 000000000..f8da6459e --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/DnsLabel.java @@ -0,0 +1,186 @@ +/* + * 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.dnslabel; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots. + *

+ * This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as + * specified in RFC 4034 § 6.1. + *

+ *

+ * Note that as per RFC 2181 § 11 DNS labels may contain + * any byte. + *

+ * + * @see RFC 5890 § 2.2. DNS-Related Terminology + * @author Florian Schmaus + * + */ +public abstract class DnsLabel implements CharSequence, Comparable { + + /** + * The maximum length of a DNS label in octets. + * + * @see RFC 1035 § 2.3.4. + */ + public static final int MAX_LABEL_LENGTH_IN_OCTETS = 63; + + public static final DnsLabel WILDCARD_LABEL = DnsLabel.from("*"); + + /** + * Whether or not the DNS label is validated on construction. + */ + public static boolean VALIDATE = true; + + public final String label; + + protected DnsLabel(String label) { + this.label = label; + + if (!VALIDATE) { + return; + } + + setBytesIfRequired(); + if (byteCache.length > MAX_LABEL_LENGTH_IN_OCTETS) { + throw new LabelToLongException(label); + } + } + + private transient String internationalizedRepresentation; + + public final String getInternationalizedRepresentation() { + if (internationalizedRepresentation == null) { + internationalizedRepresentation = getInternationalizedRepresentationInternal(); + } + return internationalizedRepresentation; + } + + protected String getInternationalizedRepresentationInternal() { + return label; + } + + public final String getLabelType() { + return getClass().getSimpleName(); + } + + @Override + public final int length() { + return label.length(); + } + + @Override + public final char charAt(int index) { + return label.charAt(index); + } + + @Override + public final CharSequence subSequence(int start, int end) { + return label.subSequence(start, end); + } + + @Override + public final String toString() { + return label; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof DnsLabel)) { + return false; + } + DnsLabel otherDnsLabel = (DnsLabel) other; + return label.equals(otherDnsLabel.label); + } + + @Override + public final int hashCode() { + return label.hashCode(); + } + + private transient DnsLabel lowercasedVariant; + + public final DnsLabel asLowercaseVariant() { + if (lowercasedVariant == null) { + String lowercaseLabel = label.toLowerCase(Locale.US); + lowercasedVariant = DnsLabel.from(lowercaseLabel); + } + return lowercasedVariant; + } + + private transient byte[] byteCache; + + private void setBytesIfRequired() { + if (byteCache == null) { + byteCache = label.getBytes(StandardCharsets.US_ASCII); + } + } + + public final void writeToBoas(ByteArrayOutputStream byteArrayOutputStream) { + setBytesIfRequired(); + + byteArrayOutputStream.write(byteCache.length); + byteArrayOutputStream.write(byteCache, 0, byteCache.length); + } + + @Override + public final int compareTo(DnsLabel other) { + String myCanonical = asLowercaseVariant().label; + String otherCanonical = other.asLowercaseVariant().label; + + return myCanonical.compareTo(otherCanonical); + } + + public static DnsLabel from(String label) { + if (label == null || label.isEmpty()) { + throw new IllegalArgumentException("Label is null or empty"); + } + + if (LdhLabel.isLdhLabel(label)) { + return LdhLabel.fromInternal(label); + } + + return NonLdhLabel.fromInternal(label); + } + + public static DnsLabel[] from(String[] labels) { + DnsLabel[] res = new DnsLabel[labels.length]; + + for (int i = 0; i < labels.length; i++) { + res[i] = DnsLabel.from(labels[i]); + } + + return res; + } + + public static boolean isIdnAcePrefixed(String string) { + return string.toLowerCase(Locale.US).startsWith("xn--"); + } + + public static class LabelToLongException extends IllegalArgumentException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final String label; + + LabelToLongException(String label) { + this.label = label; + } + } +} diff --git a/core/java/src/org/minidns/dnslabel/FakeALabel.java b/core/java/src/org/minidns/dnslabel/FakeALabel.java new file mode 100644 index 000000000..379ac49a2 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/FakeALabel.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.dnslabel; + +public final class FakeALabel extends XnLabel { + + protected FakeALabel(String label) { + super(label); + } + +} diff --git a/core/java/src/org/minidns/dnslabel/LdhLabel.java b/core/java/src/org/minidns/dnslabel/LdhLabel.java new file mode 100644 index 000000000..0f0795197 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/LdhLabel.java @@ -0,0 +1,61 @@ +/* + * 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.dnslabel; + +/** + * A LDH (Letters, Digits, Hyphen) label, which is the classical label form. + * + * @see RFC 5890 § 2.3.1. LDH Label + * + */ +public abstract class LdhLabel extends DnsLabel { + + protected LdhLabel(String label) { + super(label); + } + + public static boolean isLdhLabel(String label) { + if (label.isEmpty()) { + return false; + } + + if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) { + return false; + } + + for (int i = 0; i < label.length(); i++) { + char c = label.charAt(i); + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '-') { + continue; + } + return false; + } + return true; + } + + protected static LdhLabel fromInternal(String label) { + assert isLdhLabel(label); + + if (ReservedLdhLabel.isReservedLdhLabel(label)) { + // Label starts with '??--'. Now let us see if it is a XN-Label, starting with 'xn--', but be aware that the + // 'xn' part is case insensitive. The XnLabel.isXnLabelInternal(String) method takes care of this. + if (XnLabel.isXnLabelInternal(label)) { + return XnLabel.fromInternal(label); + } else { + return new ReservedLdhLabel(label); + } + } + return new NonReservedLdhLabel(label); + } +} diff --git a/core/java/src/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java b/core/java/src/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java new file mode 100644 index 000000000..f6ca90a76 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.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.dnslabel; + +/** + * A DNS label with a leading or trailing hyphen ('-'). + */ +public final class LeadingOrTrailingHyphenLabel extends NonLdhLabel { + + protected LeadingOrTrailingHyphenLabel(String label) { + super(label); + } + + protected static boolean isLeadingOrTrailingHypenLabelInternal(String label) { + if (label.isEmpty()) { + return false; + } + + if (label.charAt(0) == '-') { + return true; + } + + if (label.charAt(label.length() - 1) == '-') { + return true; + } + + return false; + } +} diff --git a/core/java/src/org/minidns/dnslabel/NonLdhLabel.java b/core/java/src/org/minidns/dnslabel/NonLdhLabel.java new file mode 100644 index 000000000..7ea35ca43 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/NonLdhLabel.java @@ -0,0 +1,35 @@ +/* + * 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.dnslabel; + +/** + * A DNS label which contains more than just letters, digits and a hyphen. + * + */ +public abstract class NonLdhLabel extends DnsLabel { + + protected NonLdhLabel(String label) { + super(label); + } + + protected static DnsLabel fromInternal(String label) { + if (UnderscoreLabel.isUnderscoreLabelInternal(label)) { + return new UnderscoreLabel(label); + } + + if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) { + return new LeadingOrTrailingHyphenLabel(label); + } + + return new OtherNonLdhLabel(label); + } + +} diff --git a/core/java/src/org/minidns/dnslabel/NonReservedLdhLabel.java b/core/java/src/org/minidns/dnslabel/NonReservedLdhLabel.java new file mode 100644 index 000000000..bd7fd9150 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/NonReservedLdhLabel.java @@ -0,0 +1,34 @@ +/* + * 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.dnslabel; + +/** + * A Non-Reserved LDH label (NR-LDH label), which do not have "--" in the third and fourth position. + * + */ +public final class NonReservedLdhLabel extends LdhLabel { + + protected NonReservedLdhLabel(String label) { + super(label); + assert isNonReservedLdhLabelInternal(label); + } + + public static boolean isNonReservedLdhLabel(String label) { + if (!isLdhLabel(label)) { + return false; + } + return isNonReservedLdhLabelInternal(label); + } + + static boolean isNonReservedLdhLabelInternal(String label) { + return !ReservedLdhLabel.isReservedLdhLabelInternal(label); + } +} diff --git a/core/java/src/org/minidns/dnslabel/OtherNonLdhLabel.java b/core/java/src/org/minidns/dnslabel/OtherNonLdhLabel.java new file mode 100644 index 000000000..e5ec3218a --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/OtherNonLdhLabel.java @@ -0,0 +1,23 @@ +/* + * 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.dnslabel; + +/** + * A Non-LDH label which does not begin with an underscore ('_'), hyphen ('-') or ends with an hyphen. + * + */ +public final class OtherNonLdhLabel extends NonLdhLabel { + + protected OtherNonLdhLabel(String label) { + super(label); + } + +} diff --git a/core/java/src/org/minidns/dnslabel/ReservedLdhLabel.java b/core/java/src/org/minidns/dnslabel/ReservedLdhLabel.java new file mode 100644 index 000000000..b6318e465 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/ReservedLdhLabel.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.dnslabel; + +/** + * A reserved LDH label (R-LDH label), which have the property that they contain "--" in the third and fourth characters. + * + */ +public class ReservedLdhLabel extends LdhLabel { + + protected ReservedLdhLabel(String label) { + super(label); + assert isReservedLdhLabelInternal(label); + } + + public static boolean isReservedLdhLabel(String label) { + if (!isLdhLabel(label)) { + return false; + } + return isReservedLdhLabelInternal(label); + } + + static boolean isReservedLdhLabelInternal(String label) { + return label.length() >= 4 + && label.charAt(2) == '-' + && label.charAt(3) == '-'; + } +} diff --git a/core/java/src/org/minidns/dnslabel/UnderscoreLabel.java b/core/java/src/org/minidns/dnslabel/UnderscoreLabel.java new file mode 100644 index 000000000..0a885f1e9 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/UnderscoreLabel.java @@ -0,0 +1,26 @@ +/* + * 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.dnslabel; + +/** + * A DNS label which begins with an underscore ('_'). + * + */ +public final class UnderscoreLabel extends NonLdhLabel { + + protected UnderscoreLabel(String label) { + super(label); + } + + protected static boolean isUnderscoreLabelInternal(String label) { + return label.charAt(0) == '_'; + } +} diff --git a/core/java/src/org/minidns/dnslabel/XnLabel.java b/core/java/src/org/minidns/dnslabel/XnLabel.java new file mode 100644 index 000000000..42fbbff72 --- /dev/null +++ b/core/java/src/org/minidns/dnslabel/XnLabel.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.dnslabel; + +import java.util.Locale; + +import org.minidns.idna.MiniDnsIdna; + +/** + * A label that begins with "xn--" and follows the LDH rule. + */ +public abstract class XnLabel extends ReservedLdhLabel { + + protected XnLabel(String label) { + super(label); + } + + protected static LdhLabel fromInternal(String label) { + assert isIdnAcePrefixed(label); + + String uLabel = MiniDnsIdna.toUnicode(label); + if (label.equals(uLabel)) { + // No Punycode conversation to Unicode was performed, this is a fake A-label! + return new FakeALabel(label); + } else { + return new ALabel(label); + } + } + + public static boolean isXnLabel(String label) { + if (!isLdhLabel(label)) { + return false; + } + return isXnLabelInternal(label); + } + + static boolean isXnLabelInternal(String label) { + return label.substring(0, 2).toLowerCase(Locale.US).equals("xn"); + } +} diff --git a/core/java/src/org/minidns/dnsmessage/DnsMessage.java b/core/java/src/org/minidns/dnsmessage/DnsMessage.java new file mode 100644 index 000000000..83e622de1 --- /dev/null +++ b/core/java/src/org/minidns/dnsmessage/DnsMessage.java @@ -0,0 +1,1279 @@ +/* + * 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 org.minidns.edns.Edns; +import org.minidns.record.Data; +import org.minidns.record.OPT; +import org.minidns.record.Record; +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.io.OutputStream; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A DNS message as defined by RFC 1035. The message consists of a header and + * 4 sections: question, answer, nameserver and addition resource record + * section. + * A message can either be parsed ({@link #DnsMessage(byte[])}) or serialized + * ({@link DnsMessage#toArray()}). + * + * @see RFC 1035 + */ +public class DnsMessage { + + private static final Logger LOGGER = Logger.getLogger(DnsMessage.class.getName()); + + /** + * Possible DNS response codes. + * + * @see + * IANA Domain Name System (DNS) Paramters - DNS RCODEs + * @see RFC 6895 § 2.3 + */ + public enum RESPONSE_CODE { + NO_ERROR(0), + FORMAT_ERR(1), + SERVER_FAIL(2), + NX_DOMAIN(3), + NO_IMP(4), + REFUSED(5), + YXDOMAIN(6), + YXRRSET(7), + NXRRSET(8), + NOT_AUTH(9), + NOT_ZONE(10), + BADVERS_BADSIG(16), + BADKEY(17), + BADTIME(18), + BADMODE(19), + BADNAME(20), + BADALG(21), + BADTRUNC(22), + BADCOOKIE(23), + ; + + /** + * Reverse lookup table for response codes. + */ + private static final Map INVERSE_LUT = new HashMap<>(RESPONSE_CODE.values().length); + + static { + for (RESPONSE_CODE responseCode : RESPONSE_CODE.values()) { + INVERSE_LUT.put((int) responseCode.value, responseCode); + } + } + + /** + * The response code value. + */ + private final byte value; + + /** + * Create a new response code. + * + * @param value The response code value. + */ + RESPONSE_CODE(int value) { + this.value = (byte) value; + } + + /** + * Retrieve the byte value of the response code. + * + * @return the response code. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the response code for a byte value. + * + * @param value The byte value. + * @return The symbolic response code or null. + * @throws IllegalArgumentException if the value is not in the range of 0..15. + */ + public static RESPONSE_CODE getResponseCode(int value) throws IllegalArgumentException { + if (value < 0 || value > 65535) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT.get(value); + } + + } + + /** + * Symbolic DNS Opcode values. + * + * @see + * IANA Domain Name System (DNS) Paramters - DNS OpCodes + */ + public enum OPCODE { + QUERY, + INVERSE_QUERY, + STATUS, + UNASSIGNED3, + NOTIFY, + UPDATE, + ; + + /** + * Lookup table for for opcode resolution. + */ + private static final OPCODE[] INVERSE_LUT = new OPCODE[OPCODE.values().length]; + + static { + for (OPCODE opcode : OPCODE.values()) { + if (INVERSE_LUT[opcode.getValue()] != null) { + throw new IllegalStateException(); + } + INVERSE_LUT[opcode.getValue()] = opcode; + } + } + + /** + * The value of this opcode. + */ + private final byte value; + + /** + * Create a new opcode for a given byte value. + * + */ + OPCODE() { + this.value = (byte) this.ordinal(); + } + + /** + * Retrieve the byte value of this opcode. + * + * @return The byte value of this opcode. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the symbolic name of an opcode byte. + * + * @param value The byte value of the opcode. + * @return The symbolic opcode or null. + * @throws IllegalArgumentException If the byte value is not in the + * range 0..15. + */ + public static OPCODE getOpcode(int value) throws IllegalArgumentException { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + if (value >= INVERSE_LUT.length) { + return null; + } + return INVERSE_LUT[value]; + } + + } + + /** + * The DNS message id. + */ + public final int id; + + /** + * The DNS message opcode. + */ + public final OPCODE opcode; + + /** + * The response code of this dns message. + */ + public final RESPONSE_CODE responseCode; + + /** + * The QR flag of the DNS message header. Note that this will be true if the message is a + * response and false if it is a query. + * + * @see RFC 1035 § 4.1.1 + */ + public final boolean qr; + + /** + * True if this is a authorative response. If set, the responding nameserver is an authority for the domain name in + * the question section. Note that the answer section may have multiple owner names because of aliases. This flag + * corresponds to the name which matches the query name, or the first owner name in the query section. + * + * @see RFC 1035 § 4.1.1. Header section format + */ + public final boolean authoritativeAnswer; + + /** + * True if message is truncated. Then TCP should be used. + */ + public final boolean truncated; + + /** + * True if the server should recurse. + */ + public final boolean recursionDesired; + + /** + * True if recursion is possible. + */ + public final boolean recursionAvailable; + + /** + * True if the server regarded the response as authentic. + */ + public final boolean authenticData; + + /** + * True if the server should not perform DNSSEC validation before returning the result. + */ + public final boolean checkingDisabled; + + /** + * The question section content. Usually there will be only one question. + *

+ * This list is unmodifiable. + *

+ */ + public final List questions; + + /** + * The answers section records. Note that it is not guaranteed that all records found in this section will be direct + * answers to the question in the query. If DNSSEC is used, then this section also contains the RRSIG record. + *

+ * This list is unmodifiable. + *

+ */ + public final List> answerSection; + + /** + * The Authority Section. Note that it is not guaranteed that this section only contains nameserver records. If DNSSEC is used, then this section could also contain a NSEC(3) record. + *

+ * This list is unmodifiable. + *

+ */ + public final List> authoritySection; + + /** + * The additional section. It eventually contains RRs which relate to the query. + *

+ * This list is unmodifiable. + *

+ */ + public final List> additionalSection; + + public final int optRrPosition; + + /** + * The optional but very common EDNS information. Note that this field is lazily populated. + * + */ + private Edns edns; + + /** + * The receive timestamp. Set only if this message was created via parse. + * This should be used to evaluate TTLs. + */ + public final long receiveTimestamp; + + protected DnsMessage(Builder builder) { + this.id = builder.id; + this.opcode = builder.opcode; + this.responseCode = builder.responseCode; + this.receiveTimestamp = builder.receiveTimestamp; + this.qr = builder.query; + this.authoritativeAnswer = builder.authoritativeAnswer; + this.truncated = builder.truncated; + this.recursionDesired = builder.recursionDesired; + this.recursionAvailable = builder.recursionAvailable; + this.authenticData = builder.authenticData; + this.checkingDisabled = builder.checkingDisabled; + + if (builder.questions == null) { + this.questions = Collections.emptyList(); + } else { + List q = new ArrayList<>(builder.questions.size()); + q.addAll(builder.questions); + this.questions = Collections.unmodifiableList(q); + } + + if (builder.answerSection == null) { + this.answerSection = Collections.emptyList(); + } else { + List> a = new ArrayList<>(builder.answerSection.size()); + a.addAll(builder.answerSection); + this.answerSection = Collections.unmodifiableList(a); + } + + if (builder.authoritySection == null) { + this.authoritySection = Collections.emptyList(); + } else { + List> n = new ArrayList<>(builder.authoritySection.size()); + n.addAll(builder.authoritySection); + this.authoritySection = Collections.unmodifiableList(n); + } + + if (builder.additionalSection == null && builder.ednsBuilder == null) { + this.additionalSection = Collections.emptyList(); + } else { + int size = 0; + if (builder.additionalSection != null) { + size += builder.additionalSection.size(); + } + if (builder.ednsBuilder != null) { + size++; + } + List> a = new ArrayList<>(size); + if (builder.additionalSection != null) { + a.addAll(builder.additionalSection); + } + if (builder.ednsBuilder != null) { + Edns edns = builder.ednsBuilder.build(); + this.edns = edns; + a.add(edns.asRecord()); + } + this.additionalSection = Collections.unmodifiableList(a); + } + + optRrPosition = getOptRrPosition(this.additionalSection); + + if (optRrPosition != -1) { + // Verify that there are no further OPT records but the one we already found. + for (int i = optRrPosition + 1; i < this.additionalSection.size(); i++) { + if (this.additionalSection.get(i).type == TYPE.OPT) { + throw new IllegalArgumentException("There must be only one OPT pseudo RR in the additional section"); + } + } + } + + // TODO Add verification of dns message state here + } + + /** + * Build a DNS Message based on a binary DNS message. + * + * @param data The DNS message data. + * @throws IOException On read errors. + */ + public DnsMessage(byte[] data) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bis); + id = dis.readUnsignedShort(); + int header = dis.readUnsignedShort(); + qr = ((header >> 15) & 1) == 1; + opcode = OPCODE.getOpcode((header >> 11) & 0xf); + authoritativeAnswer = ((header >> 10) & 1) == 1; + truncated = ((header >> 9) & 1) == 1; + recursionDesired = ((header >> 8) & 1) == 1; + recursionAvailable = ((header >> 7) & 1) == 1; + authenticData = ((header >> 5) & 1) == 1; + checkingDisabled = ((header >> 4) & 1) == 1; + responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + receiveTimestamp = System.currentTimeMillis(); + int questionCount = dis.readUnsignedShort(); + int answerCount = dis.readUnsignedShort(); + int nameserverCount = dis.readUnsignedShort(); + int additionalResourceRecordCount = dis.readUnsignedShort(); + questions = new ArrayList<>(questionCount); + for (int i = 0; i < questionCount; i++) { + questions.add(new Question(dis, data)); + } + answerSection = new ArrayList<>(answerCount); + for (int i = 0; i < answerCount; i++) { + answerSection.add(Record.parse(dis, data)); + } + authoritySection = new ArrayList<>(nameserverCount); + for (int i = 0; i < nameserverCount; i++) { + authoritySection.add(Record.parse(dis, data)); + } + additionalSection = new ArrayList<>(additionalResourceRecordCount); + for (int i = 0; i < additionalResourceRecordCount; i++) { + additionalSection.add(Record.parse(dis, data)); + } + optRrPosition = getOptRrPosition(additionalSection); + } + + /** + * Constructs an normalized version of the given DnsMessage by setting the id to '0'. + * + * @param message the message of which normalized version should be constructed. + */ + private DnsMessage(DnsMessage message) { + id = 0; + qr = message.qr; + opcode = message.opcode; + authoritativeAnswer = message.authoritativeAnswer; + truncated = message.truncated; + recursionDesired = message.recursionDesired; + recursionAvailable = message.recursionAvailable; + authenticData = message.authenticData; + checkingDisabled = message.checkingDisabled; + responseCode = message.responseCode; + receiveTimestamp = message.receiveTimestamp; + questions = message.questions; + answerSection = message.answerSection; + authoritySection = message.authoritySection; + additionalSection = message.additionalSection; + optRrPosition = message.optRrPosition; + } + + private static int getOptRrPosition(List> additionalSection) { + int optRrPosition = -1; + for (int i = 0; i < additionalSection.size(); i++) { + Record record = additionalSection.get(i); + if (record.type == Record.TYPE.OPT) { + optRrPosition = i; + break; + } + } + return optRrPosition; + } + + /** + * Generate a binary dns packet out of this message. + * + * @return byte[] the binary representation. + */ + public byte[] toArray() { + return serialize().clone(); + } + + public DatagramPacket asDatagram(InetAddress address, int port) { + byte[] bytes = serialize(); + return new DatagramPacket(bytes, bytes.length, address, port); + } + + public void writeTo(OutputStream outputStream) throws IOException { + writeTo(outputStream, true); + } + + public void writeTo(OutputStream outputStream, boolean writeLength) throws IOException { + byte[] bytes = serialize(); + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + if (writeLength) { + dataOutputStream.writeShort(bytes.length); + } + dataOutputStream.write(bytes); + } + + public ByteBuffer getInByteBuffer() { + byte[] bytes = serialize().clone(); + return ByteBuffer.wrap(bytes); + } + + private byte[] byteCache; + + private byte[] serialize() { + if (byteCache != null) { + return byteCache; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + int header = calculateHeaderBitmap(); + try { + dos.writeShort((short) id); + dos.writeShort((short) header); + if (questions == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) questions.size()); + } + if (answerSection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) answerSection.size()); + } + if (authoritySection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) authoritySection.size()); + } + if (additionalSection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) additionalSection.size()); + } + if (questions != null) { + for (Question question : questions) { + dos.write(question.toByteArray()); + } + } + if (answerSection != null) { + for (Record answer : answerSection) { + dos.write(answer.toByteArray()); + } + } + if (authoritySection != null) { + for (Record nameserverRecord : authoritySection) { + dos.write(nameserverRecord.toByteArray()); + } + } + if (additionalSection != null) { + for (Record additionalResourceRecord : additionalSection) { + dos.write(additionalResourceRecord.toByteArray()); + } + } + dos.flush(); + } catch (IOException e) { + // Should never happen. + throw new AssertionError(e); + } + byteCache = baos.toByteArray(); + return byteCache; + } + + int calculateHeaderBitmap() { + int header = 0; + if (qr) { + header += 1 << 15; + } + if (opcode != null) { + header += opcode.getValue() << 11; + } + if (authoritativeAnswer) { + header += 1 << 10; + } + if (truncated) { + header += 1 << 9; + } + if (recursionDesired) { + header += 1 << 8; + } + if (recursionAvailable) { + header += 1 << 7; + } + if (authenticData) { + header += 1 << 5; + } + if (checkingDisabled) { + header += 1 << 4; + } + if (responseCode != null) { + header += responseCode.getValue(); + } + return header; + } + + public Question getQuestion() { + return questions.get(0); + } + + /** + * Copy the questions found in the question section. + * + * @return a copy of the question section questions. + * @see #questions + */ + public List copyQuestions() { + List copy = new ArrayList<>(questions.size()); + copy.addAll(questions); + return copy; + } + + /** + * Copy the records found in the answer section into a new list. + * + * @return a copy of the answer section records. + * @see #answerSection + */ + public List> copyAnswers() { + List> res = new ArrayList<>(answerSection.size()); + res.addAll(answerSection); + return res; + } + + /** + * Copy the records found in the authority section into a new list. + * + * @return a copy of the authority section records. + * @see #authoritySection + */ + public List> copyAuthority() { + List> res = new ArrayList<>(authoritySection.size()); + res.addAll(authoritySection); + return res; + } + + public Edns getEdns() { + if (edns != null) return edns; + + Record optRecord = getOptPseudoRecord(); + if (optRecord == null) return null; + edns = new Edns(optRecord); + return edns; + } + + @SuppressWarnings("unchecked") + public Record getOptPseudoRecord() { + if (optRrPosition == -1) return null; + return (Record) additionalSection.get(optRrPosition); + } + + /** + * Check if the EDNS DO (DNSSEC OK) flag is set. + * + * @return true if the DO flag is set. + */ + public boolean isDnssecOk() { + Edns edns = getEdns(); + if (edns == null) + return false; + + return edns.dnssecOk; + } + + private String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) return toStringCache; + + StringBuilder sb = new StringBuilder("DnsMessage"); + asBuilder().writeToStringBuilder(sb); + + toStringCache = sb.toString(); + return toStringCache; + } + + private String terminalOutputCache; + + /** + * Format the DnsMessage object in a way suitable for terminal output. + * The format is loosely based on the output provided by {@code dig}. + * + * @return This message as a String suitable for terminal output. + */ + public String asTerminalOutput() { + if (terminalOutputCache != null) return terminalOutputCache; + + StringBuilder sb = new StringBuilder(";; ->>HEADER<<-") + .append(" opcode: ").append(opcode) + .append(", status: ").append(responseCode) + .append(", id: ").append(id).append("\n") + .append(";; flags:"); + if (!qr) sb.append(" qr"); + 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("; QUERY: ").append(questions.size()) + .append(", ANSWER: ").append(answerSection.size()) + .append(", AUTHORITY: ").append(authoritySection.size()) + .append(", ADDITIONAL: ").append(additionalSection.size()) + .append("\n\n"); + for (Record record : additionalSection) { + Edns edns = Edns.fromRecord(record); + if (edns != null) { + sb.append(";; OPT PSEUDOSECTION:\n; ").append(edns.asTerminalOutput()); + break; + } + } + if (questions.size() != 0) { + sb.append(";; QUESTION SECTION:\n"); + for (Question question : questions) { + sb.append(';').append(question.toString()).append('\n'); + } + } + if (authoritySection.size() != 0) { + sb.append("\n;; AUTHORITY SECTION:\n"); + for (Record record : authoritySection) { + sb.append(record.toString()).append('\n'); + } + } + if (answerSection.size() != 0) { + sb.append("\n;; ANSWER SECTION:\n"); + for (Record record : answerSection) { + sb.append(record.toString()).append('\n'); + } + } + if (additionalSection.size() != 0) { + boolean hasNonOptArr = false; + for (Record record : additionalSection) { + if (record.type != Record.TYPE.OPT) { + if (!hasNonOptArr) { + hasNonOptArr = true; + sb.append("\n;; ADDITIONAL SECTION:\n"); + } + sb.append(record.toString()).append('\n'); + } + } + } + if (receiveTimestamp > 0) { + sb.append("\n;; WHEN: ").append(new Date(receiveTimestamp).toString()); + } + terminalOutputCache = sb.toString(); + return terminalOutputCache; + } + + public Set getAnswersFor(Question q) { + if (responseCode != RESPONSE_CODE.NO_ERROR) return null; + + // It would be great if we could verify that D matches q.type at this + // point. But on the other hand, if it does not, then the cast to D + // below will fail. + Set res = new HashSet<>(answerSection.size()); + for (Record record : answerSection) { + if (!record.isAnswer(q)) continue; + + Data data = record.getPayload(); + @SuppressWarnings("unchecked") + D d = (D) data; + boolean isNew = res.add(d); + if (!isNew) { + LOGGER.log(Level.WARNING, "DnsMessage contains duplicate answers. Record: " + record + "; DnsMessage: " + this); + } + } + return res; + } + + private long answersMinTtlCache = -1; + + /** + * Get the minimum TTL from all answers in seconds. + * + * @return the minimum TTL from all answers in seconds. + */ + public long getAnswersMinTtl() { + if (answersMinTtlCache >= 0) { + return answersMinTtlCache; + } + + answersMinTtlCache = Long.MAX_VALUE; + for (Record r : answerSection) { + answersMinTtlCache = Math.min(answersMinTtlCache, r.ttl); + } + return answersMinTtlCache; + } + + public Builder asBuilder() { + return new Builder(this); + } + + private DnsMessage normalizedVersionCache; + + public DnsMessage asNormalizedVersion() { + if (normalizedVersionCache == null) { + normalizedVersionCache = new DnsMessage(this); + } + return normalizedVersionCache; + } + + public Builder getResponseBuilder(RESPONSE_CODE responseCode) { + if (qr) { + throw new IllegalStateException(); + } + Builder responseBuilder = DnsMessage.builder() + .setQrFlag(true) + .setResponseCode(responseCode) + .setId(id) + .setQuestion(getQuestion()); + + return responseBuilder; + } + + private transient Integer hashCodeCache; + + @Override + public int hashCode() { + if (hashCodeCache == null) { + byte[] bytes = serialize(); + hashCodeCache = Arrays.hashCode(bytes); + } + return hashCodeCache; + } + + private enum SectionName { + answer, + authority, + additional, + } + + private List> filterSectionByType(boolean stopOnFirst, SectionName sectionName, Class type) { + List> sectionToFilter; + switch (sectionName) { + case answer: + sectionToFilter = answerSection; + break; + case authority: + sectionToFilter = authoritySection; + break; + case additional: + sectionToFilter = additionalSection; + break; + default: + throw new AssertionError("Unknown section name " + sectionName); + } + + List> res = new ArrayList<>(stopOnFirst ? 1 : sectionToFilter.size()); + + for (Record record : sectionToFilter) { + Record target = record.ifPossibleAs(type); + if (target != null) { + res.add(target); + if (stopOnFirst) { + return res; + } + } + } + + return res; + } + + private List> filterSectionByType(SectionName sectionName, Class type) { + return filterSectionByType(false, sectionName, type); + } + + private Record getFirstOfType(SectionName sectionName, Class type) { + List> result = filterSectionByType(true, sectionName, type); + if (result.isEmpty()) { + return null; + } + + return result.get(0); + } + + public List> filterAnswerSectionBy(Class type) { + return filterSectionByType(SectionName.answer, type); + } + + public List> filterAuthoritySectionBy(Class type) { + return filterSectionByType(SectionName.authority, type); + } + + public List> filterAdditionalSectionBy(Class type) { + return filterSectionByType(SectionName.additional, type); + } + + public Record getFirstOfTypeFromAnswerSection(Class type) { + return getFirstOfType(SectionName.answer, type); + } + + public Record getFirstOfTypeFromAuthoritySection(Class type) { + return getFirstOfType(SectionName.authority, type); + } + + public Record getFirstOfTypeFromAdditionalSection(Class type) { + return getFirstOfType(SectionName.additional, type); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof DnsMessage)) { + return false; + } + if (other == this) { + return true; + } + DnsMessage otherDnsMessage = (DnsMessage) other; + byte[] otherBytes = otherDnsMessage.serialize(); + byte[] myBytes = serialize(); + return Arrays.equals(myBytes, otherBytes); + } + + public static Builder builder() { + return new DnsMessage.Builder(); + } + + public static final class Builder { + + private Builder() { + } + + private Builder(DnsMessage message) { + id = message.id; + opcode = message.opcode; + responseCode = message.responseCode; + query = message.qr; + authoritativeAnswer = message.authoritativeAnswer; + truncated = message.truncated; + recursionDesired = message.recursionDesired; + recursionAvailable = message.recursionAvailable; + authenticData = message.authenticData; + checkingDisabled = message.checkingDisabled; + receiveTimestamp = message.receiveTimestamp; + + // Copy the unmodifiable lists over into this new builder. + questions = new ArrayList<>(message.questions.size()); + questions.addAll(message.questions); + answerSection = new ArrayList<>(message.answerSection.size()); + answerSection.addAll(message.answerSection); + authoritySection = new ArrayList<>(message.authoritySection.size()); + authoritySection.addAll(message.authoritySection); + additionalSection = new ArrayList<>(message.additionalSection.size()); + additionalSection.addAll(message.additionalSection); + } + + private int id; + private OPCODE opcode = OPCODE.QUERY; + private RESPONSE_CODE responseCode = RESPONSE_CODE.NO_ERROR; + private boolean query; + private boolean authoritativeAnswer; + private boolean truncated; + private boolean recursionDesired; + private boolean recursionAvailable; + private boolean authenticData; + private boolean checkingDisabled; + + private long receiveTimestamp = -1; + + private List questions; + private List> answerSection; + private List> authoritySection; + private List> additionalSection; + private Edns.Builder ednsBuilder; + + /** + * Set the current DNS message id. + * + * @param id The new DNS message id. + * @return a reference to this builder. + */ + public Builder setId(int id) { + this.id = id & 0xffff; + return this; + } + + public Builder setOpcode(OPCODE opcode) { + this.opcode = opcode; + return this; + } + + public Builder setResponseCode(RESPONSE_CODE responseCode) { + this.responseCode = responseCode; + return this; + } + + /** + * Set the QR flag. Note that this will be true if the message is a + * response and false if it is a query. + * + * @param query The new QR flag status. + * @return a reference to this builder. + */ + public Builder setQrFlag(boolean query) { + this.query = query; + return this; + } + + /** + * Set the authoritative answer flag. + * + * @param authoritativeAnswer Tge new authoritative answer value. + * @return a reference to this builder. + */ + public Builder setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + return this; + } + + /** + * Set the truncation bit on this DNS message. + * + * @param truncated The new truncated bit status. + * @return a reference to this builder. + */ + public Builder setTruncated(boolean truncated) { + this.truncated = truncated; + return this; + } + + /** + * Set the recursion desired flag on this message. + * + * @param recursionDesired The new recusrion setting. + * @return a reference to this builder. + */ + public Builder setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Set the recursion available flog from this DNS message. + * + * @param recursionAvailable The new recursion available status. + * @return a reference to this builder. + */ + public Builder setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + return this; + } + + /** + * Set the authentic data flag on this DNS message. + * + * @param authenticData The new authentic data flag value. + * @return a reference to this builder. + */ + public Builder setAuthenticData(boolean authenticData) { + this.authenticData = authenticData; + return this; + } + + /** + * Change the check status of this packet. + * + * @param checkingDisabled The new check disabled value. + * @return a reference to this builder. + */ + @Deprecated + public Builder setCheckDisabled(boolean checkingDisabled) { + this.checkingDisabled = checkingDisabled; + return this; + } + + /** + * Change the check status of this packet. + * + * @param checkingDisabled The new check disabled value. + * @return a reference to this builder. + */ + public Builder setCheckingDisabled(boolean checkingDisabled) { + this.checkingDisabled = checkingDisabled; + return this; + } + + public void copyFlagsFrom(DnsMessage dnsMessage) { + this.query = dnsMessage.qr; + this.authoritativeAnswer = dnsMessage.authenticData; + this.truncated = dnsMessage.truncated; + this.recursionDesired = dnsMessage.recursionDesired; + this.recursionAvailable = dnsMessage.recursionAvailable; + this.authenticData = dnsMessage.authenticData; + this.checkingDisabled = dnsMessage.checkingDisabled; + } + + public Builder setReceiveTimestamp(long receiveTimestamp) { + this.receiveTimestamp = receiveTimestamp; + return this; + } + + public Builder addQuestion(Question question) { + if (questions == null) { + questions = new ArrayList<>(1); + } + questions.add(question); + return this; + } + + /** + * Set the question part of this message. + * + * @param questions The questions. + * @return a reference to this builder. + */ + public Builder setQuestions(List questions) { + this.questions = questions; + return this; + } + + /** + * Set the question part of this message. + * + * @param question The question. + * @return a reference to this builder. + */ + public Builder setQuestion(Question question) { + this.questions = new ArrayList<>(1); + this.questions.add(question); + return this; + } + + public Builder addAnswer(Record answer) { + if (answerSection == null) { + answerSection = new ArrayList<>(1); + } + answerSection.add(answer); + return this; + } + + public Builder addAnswers(Collection> records) { + if (answerSection == null) { + answerSection = new ArrayList<>(records.size()); + } + answerSection.addAll(records); + return this; + } + + public Builder setAnswers(Collection> records) { + answerSection = new ArrayList<>(records.size()); + answerSection.addAll(records); + return this; + } + + public List> getAnswers() { + if (answerSection == null) { + return Collections.emptyList(); + } + return answerSection; + } + + public Builder addNameserverRecords(Record record) { + if (authoritySection == null) { + authoritySection = new ArrayList<>(8); + } + authoritySection.add(record); + return this; + } + + public Builder setNameserverRecords(Collection> records) { + authoritySection = new ArrayList<>(records.size()); + authoritySection.addAll(records); + return this; + } + + public Builder setAdditionalResourceRecords(Collection> records) { + additionalSection = new ArrayList<>(records.size()); + additionalSection.addAll(records); + return this; + } + + public Builder addAdditionalResourceRecord(Record record) { + if (additionalSection == null) { + additionalSection = new ArrayList<>(); + } + additionalSection.add(record); + return this; + } + + public Builder addAdditionalResourceRecords(List> records) { + if (additionalSection == null) { + additionalSection = new ArrayList<>(records.size()); + } + additionalSection.addAll(records); + return this; + } + + public List> 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 record : answerSection) { + sb.append("[A: ").append(record).append("]\n"); + } + } + if (authoritySection != null) { + for (Record record : authoritySection) { + sb.append("[N: ").append(record).append("]\n"); + } + } + if (additionalSection != null) { + for (Record 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 clazz; + + OptionCode(int optionCode, Class 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 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 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 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); + } +}