org.minidns.dnsname.DnsName Maven / Gradle / Ivy
/*
* Copyright 2015-2024 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;
import org.minidns.util.SafeCharSequence;
/**
* A DNS name, also called "domain name". A DNS name consists of multiple 'labels' (see {@link DnsLabel}) 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:
*
* - UTS #46 - Unicode IDNA Compatibility Processing
*
- RFC 8753 - Internationalized Domain Names for Applications (IDNA) Review for New Unicode Versions
*
*
* @see RFC 3696
* @see DnsLabel
* @author Florian Schmaus
*
*/
public final class DnsName extends SafeCharSequence implements 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);
}
}
/**
* Return the ACE (ASCII Compatible Encoding) version of this DNS name. Note
* that this method may return a String containing null bytes. Those Strings are
* notoriously difficult to handle from a security perspective. Therefore it is
* recommended to use {@link #toString()} instead, which will return a sanitized
* String.
*
* @return the ACE version of this DNS name.
* @since 1.1.0
*/
public String getAce() {
return ace;
}
/**
* Returns the raw ACE version of this DNS name. That is, the version as it was
* received over the wire. Most notably, this version may include uppercase
* letters.
*
* Please refer to {@link #getAce()} for a discussion of the security
* implications when working with the ACE representation of a DNS name.
*
* @return the raw ACE version of this DNS name.
* @see #getAce()
*/
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 - 1];
}
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;
}
private transient String safeToStringRepresentation;
@Override
public String toString() {
if (safeToStringRepresentation == null) {
setLabelsIfRequired();
if (labels.length == 0) {
return ".";
}
StringBuilder sb = new StringBuilder();
for (int i = labels.length - 1; i >= 0; i--) {
// Note that it is important that we append the result of DnsLabel.toString() to
// the StringBuilder. As only the result of toString() is the safe label
// representation.
String safeLabelRepresentation = labels[i].toString();
sb.append(safeLabelRepresentation);
if (i != 0) {
sb.append('.');
}
}
safeToStringRepresentation = sb.toString();
}
return safeToStringRepresentation;
}
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(CharSequence child, DnsName parent) {
DnsLabel childLabel = DnsLabel.from(child.toString());
return DnsName.from(childLabel, parent);
}
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[parent.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(".");
}
}