All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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: * * * @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("."); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy