001/* 002 * Copyright 2015-2024 the original author or authors 003 * 004 * This software is licensed under the Apache License, Version 2.0, 005 * the GNU Lesser General Public License version 2 or later ("LGPL") 006 * and the WTFPL. 007 * You may choose either license to govern your use of this software only 008 * upon the condition that you accept all of the terms of either 009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. 010 */ 011package org.minidns.dnsname; 012 013import java.io.ByteArrayOutputStream; 014import java.io.DataInputStream; 015import java.io.IOException; 016import java.io.OutputStream; 017import java.io.Serializable; 018import java.nio.charset.StandardCharsets; 019import java.util.Arrays; 020import java.util.HashSet; 021import java.util.Locale; 022 023import org.minidns.dnslabel.DnsLabel; 024import org.minidns.idna.MiniDnsIdna; 025import org.minidns.util.SafeCharSequence; 026 027/** 028 * A DNS name, also called "domain name". A DNS name consists of multiple 'labels' (see {@link DnsLabel}) and is subject to certain restrictions (see 029 * for example <a href="https://tools.ietf.org/html/rfc3696#section-2">RFC 3696 § 2.</a>). 030 * <p> 031 * Instances of this class can be created by using {@link #from(String)}. 032 * </p> 033 * <p> 034 * This class holds three representations of a DNS name: ACE, raw ACE and IDN. ACE (ASCII Compatible Encoding), which 035 * can be accessed via {@link #ace}, represents mostly the data that got send over the wire. But since DNS names are 036 * case insensitive, the ACE value is normalized to lower case. You can use {@link #getRawAce()} to get the raw ACE data 037 * that was received, which possibly includes upper case characters. The IDN (Internationalized Domain Name), that is 038 * the DNS name as it should be shown to the user, can be retrieved using {@link #asIdn()}. 039 * </p> 040 * More information about Internationalized Domain Names can be found at: 041 * <ul> 042 * <li><a href="https://unicode.org/reports/tr46/">UTS #46 - Unicode IDNA Compatibility Processing</a> 043 * <li><a href="https://tools.ietf.org/html/rfc8753">RFC 8753 - Internationalized Domain Names for Applications (IDNA) Review for New Unicode Versions</a> 044 * </ul> 045 * 046 * @see <a href="https://tools.ietf.org/html/rfc3696">RFC 3696</a> 047 * @see DnsLabel 048 * @author Florian Schmaus 049 * 050 */ 051public final class DnsName extends SafeCharSequence implements Serializable, Comparable<DnsName> { 052 053 /** 054 * 055 */ 056 private static final long serialVersionUID = 1L; 057 058 /** 059 * @see <a href="https://www.ietf.org/rfc/rfc3490.txt">RFC 3490 § 3.1 1.</a> 060 */ 061 private static final String LABEL_SEP_REGEX = "[.\u3002\uFF0E\uFF61]"; 062 063 /** 064 * See <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a> 065 */ 066 static final int MAX_DNSNAME_LENGTH_IN_OCTETS = 255; 067 068 public static final int MAX_LABELS = 128; 069 070 public static final DnsName ROOT = new DnsName("."); 071 072 public static final DnsName IN_ADDR_ARPA = new DnsName("in-addr.arpa"); 073 074 public static final DnsName IP6_ARPA = new DnsName("ip6.arpa"); 075 076 /** 077 * Whether or not the DNS name is validated on construction. 078 */ 079 public static boolean VALIDATE = true; 080 081 /** 082 * The DNS name in ASCII Compatible Encoding (ACE). 083 */ 084 public final String ace; 085 086 /** 087 * The DNS name in raw format, i.e. as it was received from the remote server. This means that compared to 088 * {@link #ace}, this String may not be lower-cased. 089 */ 090 private final String rawAce; 091 092 private transient byte[] bytes; 093 094 private transient byte[] rawBytes; 095 096 private transient String idn; 097 098 private transient String domainpart; 099 100 private transient String hostpart; 101 102 /** 103 * The labels in <b>reverse</b> order. 104 */ 105 private transient DnsLabel[] labels; 106 107 private transient DnsLabel[] rawLabels; 108 109 private transient int hashCode; 110 111 private int size = -1; 112 113 private DnsName(String name) { 114 this(name, true); 115 } 116 117 private DnsName(String name, boolean inAce) { 118 if (name.isEmpty()) { 119 rawAce = ROOT.rawAce; 120 } else { 121 final int nameLength = name.length(); 122 final int nameLastPos = nameLength - 1; 123 124 // Strip potential trailing dot. N.B. that we require nameLength > 2, because we don't want to strip the one 125 // character string containing only a single dot to the empty string. 126 if (nameLength >= 2 && name.charAt(nameLastPos) == '.') { 127 name = name.subSequence(0, nameLastPos).toString(); 128 } 129 130 if (inAce) { 131 // Name is already in ACE format. 132 rawAce = name; 133 } else { 134 rawAce = MiniDnsIdna.toASCII(name); 135 } 136 } 137 138 ace = rawAce.toLowerCase(Locale.US); 139 140 if (!VALIDATE) { 141 return; 142 } 143 144 // Validate the DNS name. 145 validateMaxDnsnameLengthInOctets(); 146 } 147 148 private DnsName(DnsLabel[] rawLabels, boolean validateMaxDnsnameLength) { 149 this.rawLabels = rawLabels; 150 this.labels = new DnsLabel[rawLabels.length]; 151 152 int size = 0; 153 for (int i = 0; i < rawLabels.length; i++) { 154 size += rawLabels[i].length() + 1; 155 labels[i] = rawLabels[i].asLowercaseVariant(); 156 } 157 158 rawAce = labelsToString(rawLabels, size); 159 ace = labelsToString(labels, size); 160 161 // The following condition is deliberately designed that VALIDATE=false causes the validation to be skipped even 162 // if validateMaxDnsnameLength is set to true. There is no need to validate even if this constructor is called 163 // with validateMaxDnsnameLength set to true if VALIDATE is globally set to false. 164 if (!validateMaxDnsnameLength || !VALIDATE) { 165 return; 166 } 167 168 validateMaxDnsnameLengthInOctets(); 169 } 170 171 private static String labelsToString(DnsLabel[] labels, int stringLength) { 172 StringBuilder sb = new StringBuilder(stringLength); 173 for (int i = labels.length - 1; i >= 0; i--) { 174 sb.append(labels[i]).append('.'); 175 } 176 sb.setLength(sb.length() - 1); 177 return sb.toString(); 178 } 179 180 private void validateMaxDnsnameLengthInOctets() { 181 setBytesIfRequired(); 182 if (bytes.length > MAX_DNSNAME_LENGTH_IN_OCTETS) { 183 throw new InvalidDnsNameException.DNSNameTooLongException(ace, bytes); 184 } 185 } 186 187 public void writeToStream(OutputStream os) throws IOException { 188 setBytesIfRequired(); 189 os.write(bytes); 190 } 191 192 /** 193 * Serialize a domain name under IDN rules. 194 * 195 * @return The binary domain name representation. 196 */ 197 public byte[] getBytes() { 198 setBytesIfRequired(); 199 return bytes.clone(); 200 } 201 202 public byte[] getRawBytes() { 203 if (rawBytes == null) { 204 setLabelsIfRequired(); 205 rawBytes = toBytes(rawLabels); 206 } 207 208 return rawBytes.clone(); 209 } 210 211 private void setBytesIfRequired() { 212 if (bytes != null) 213 return; 214 215 setLabelsIfRequired(); 216 bytes = toBytes(labels); 217 } 218 219 private static byte[] toBytes(DnsLabel[] labels) { 220 ByteArrayOutputStream baos = new ByteArrayOutputStream(64); 221 for (int i = labels.length - 1; i >= 0; i--) { 222 labels[i].writeToBoas(baos); 223 } 224 225 baos.write(0); 226 227 return baos.toByteArray(); 228 } 229 230 private void setLabelsIfRequired() { 231 if (labels != null && rawLabels != null) return; 232 233 if (isRootLabel()) { 234 rawLabels = labels = new DnsLabel[0]; 235 return; 236 } 237 238 labels = getLabels(ace); 239 rawLabels = getLabels(rawAce); 240 } 241 242 private static DnsLabel[] getLabels(String ace) { 243 String[] labels = ace.split(LABEL_SEP_REGEX, MAX_LABELS); 244 245 // Reverse the labels, so that 'foo, example, org' becomes 'org, example, foo'. 246 for (int i = 0; i < labels.length / 2; i++) { 247 String t = labels[i]; 248 int j = labels.length - i - 1; 249 labels[i] = labels[j]; 250 labels[j] = t; 251 } 252 253 try { 254 return DnsLabel.from(labels); 255 } catch (DnsLabel.LabelToLongException e) { 256 throw new InvalidDnsNameException.LabelTooLongException(ace, e.label); 257 } 258 } 259 260 /** 261 * Return the ACE (ASCII Compatible Encoding) version of this DNS name. Note 262 * that this method may return a String containing null bytes. Those Strings are 263 * notoriously difficult to handle from a security perspective. Therefore it is 264 * recommended to use {@link #toString()} instead, which will return a sanitized 265 * String. 266 * 267 * @return the ACE version of this DNS name. 268 * @since 1.1.0 269 */ 270 public String getAce() { 271 return ace; 272 } 273 274 /** 275 * Returns the raw ACE version of this DNS name. That is, the version as it was 276 * received over the wire. Most notably, this version may include uppercase 277 * letters. 278 * 279 * <b>Please refer to {@link #getAce()} for a discussion of the security 280 * implications when working with the ACE representation of a DNS name.</b> 281 * 282 * @return the raw ACE version of this DNS name. 283 * @see #getAce() 284 */ 285 public String getRawAce() { 286 return rawAce; 287 } 288 289 public String asIdn() { 290 if (idn != null) 291 return idn; 292 293 idn = MiniDnsIdna.toUnicode(ace); 294 return idn; 295 } 296 297 /** 298 * Domainpart in ACE representation. 299 * 300 * @return the domainpart in ACE representation. 301 */ 302 public String getDomainpart() { 303 setHostnameAndDomainpartIfRequired(); 304 return domainpart; 305 } 306 307 /** 308 * Hostpart in ACE representation. 309 * 310 * @return the hostpart in ACE representation. 311 */ 312 public String getHostpart() { 313 setHostnameAndDomainpartIfRequired(); 314 return hostpart; 315 } 316 317 public DnsLabel getHostpartLabel() { 318 setLabelsIfRequired(); 319 return labels[labels.length - 1]; 320 } 321 322 private void setHostnameAndDomainpartIfRequired() { 323 if (hostpart != null) return; 324 325 String[] parts = ace.split(LABEL_SEP_REGEX, 2); 326 hostpart = parts[0]; 327 if (parts.length > 1) { 328 domainpart = parts[1]; 329 } else { 330 domainpart = ""; 331 } 332 } 333 334 public int size() { 335 if (size < 0) { 336 if (isRootLabel()) { 337 size = 1; 338 } else { 339 size = ace.length() + 2; 340 } 341 } 342 return size; 343 } 344 345 private transient String safeToStringRepresentation; 346 347 @Override 348 public String toString() { 349 if (safeToStringRepresentation == null) { 350 setLabelsIfRequired(); 351 if (labels.length == 0) { 352 return "."; 353 } 354 355 StringBuilder sb = new StringBuilder(); 356 for (int i = labels.length - 1; i >= 0; i--) { 357 // Note that it is important that we append the result of DnsLabel.toString() to 358 // the StringBuilder. As only the result of toString() is the safe label 359 // representation. 360 String safeLabelRepresentation = labels[i].toString(); 361 sb.append(safeLabelRepresentation); 362 if (i != 0) { 363 sb.append('.'); 364 } 365 } 366 safeToStringRepresentation = sb.toString(); 367 } 368 369 return safeToStringRepresentation; 370 } 371 372 public static DnsName from(CharSequence name) { 373 return from(name.toString()); 374 } 375 376 public static DnsName from(String name) { 377 return new DnsName(name, false); 378 } 379 380 /** 381 * Create a DNS name by "concatenating" the child under the parent name. The child can also be seen as the "left" 382 * part of the resulting DNS name and the parent is the "right" part. 383 * <p> 384 * For example using "i.am.the.child" as child and "of.this.parent.example" as parent, will result in a DNS name: 385 * "i.am.the.child.of.this.parent.example". 386 * </p> 387 * 388 * @param child the child DNS name. 389 * @param parent the parent DNS name. 390 * @return the resulting of DNS name. 391 */ 392 public static DnsName from(DnsName child, DnsName parent) { 393 child.setLabelsIfRequired(); 394 parent.setLabelsIfRequired(); 395 396 DnsLabel[] rawLabels = new DnsLabel[child.rawLabels.length + parent.rawLabels.length]; 397 System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); 398 System.arraycopy(child.rawLabels, 0, rawLabels, parent.rawLabels.length, child.rawLabels.length); 399 return new DnsName(rawLabels, true); 400 } 401 402 public static DnsName from(CharSequence child, DnsName parent) { 403 DnsLabel childLabel = DnsLabel.from(child.toString()); 404 return DnsName.from(childLabel, parent); 405 } 406 407 public static DnsName from(DnsLabel child, DnsName parent) { 408 parent.setLabelsIfRequired(); 409 410 DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 1]; 411 System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); 412 rawLabels[parent.rawLabels.length] = child; 413 return new DnsName(rawLabels, true); 414 } 415 416 public static DnsName from(DnsLabel grandchild, DnsLabel child, DnsName parent) { 417 parent.setBytesIfRequired(); 418 419 DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 2]; 420 System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); 421 rawLabels[parent.rawLabels.length] = child; 422 rawLabels[parent.rawLabels.length + 1] = grandchild; 423 return new DnsName(rawLabels, true); 424 } 425 426 public static DnsName from(DnsName... nameComponents) { 427 int labelCount = 0; 428 for (DnsName component : nameComponents) { 429 component.setLabelsIfRequired(); 430 labelCount += component.rawLabels.length; 431 } 432 433 DnsLabel[] rawLabels = new DnsLabel[labelCount]; 434 int destLabelPos = 0; 435 for (int i = nameComponents.length - 1; i >= 0; i--) { 436 DnsName component = nameComponents[i]; 437 System.arraycopy(component.rawLabels, 0, rawLabels, destLabelPos, component.rawLabels.length); 438 destLabelPos += component.rawLabels.length; 439 } 440 441 return new DnsName(rawLabels, true); 442 } 443 444 public static DnsName from(String[] parts) { 445 DnsLabel[] rawLabels = DnsLabel.from(parts); 446 447 return new DnsName(rawLabels, true); 448 } 449 450 /** 451 * Parse a domain name starting at the current offset and moving the input 452 * stream pointer past this domain name (even if cross references occure). 453 * 454 * @param dis The input stream. 455 * @param data The raw data (for cross references). 456 * @return The domain name string. 457 * @throws IOException Should never happen. 458 */ 459 public static DnsName parse(DataInputStream dis, byte[] data) 460 throws IOException { 461 int c = dis.readUnsignedByte(); 462 if ((c & 0xc0) == 0xc0) { 463 c = ((c & 0x3f) << 8) + dis.readUnsignedByte(); 464 HashSet<Integer> jumps = new HashSet<Integer>(); 465 jumps.add(c); 466 return parse(data, c, jumps); 467 } 468 if (c == 0) { 469 return DnsName.ROOT; 470 } 471 byte[] b = new byte[c]; 472 dis.readFully(b); 473 474 String childLabelString = new String(b, StandardCharsets.US_ASCII); 475 DnsName child = new DnsName(childLabelString); 476 477 DnsName parent = parse(dis, data); 478 return DnsName.from(child, parent); 479 } 480 481 /** 482 * Parse a domain name starting at the given offset. 483 * 484 * @param data The raw data. 485 * @param offset The offset. 486 * @param jumps The list of jumps (by now). 487 * @return The parsed domain name. 488 * @throws IllegalStateException on cycles. 489 */ 490 @SuppressWarnings("NonApiType") 491 private static DnsName parse(byte[] data, int offset, HashSet<Integer> jumps) 492 throws IllegalStateException { 493 int c = data[offset] & 0xff; 494 if ((c & 0xc0) == 0xc0) { 495 c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff); 496 if (jumps.contains(c)) { 497 throw new IllegalStateException("Cyclic offsets detected."); 498 } 499 jumps.add(c); 500 return parse(data, c, jumps); 501 } 502 if (c == 0) { 503 return DnsName.ROOT; 504 } 505 506 String childLabelString = new String(data, offset + 1, c, StandardCharsets.US_ASCII); 507 DnsName child = new DnsName(childLabelString); 508 509 DnsName parent = parse(data, offset + 1 + c, jumps); 510 return DnsName.from(child, parent); 511 } 512 513 @Override 514 public int compareTo(DnsName other) { 515 return ace.compareTo(other.ace); 516 } 517 518 @Override 519 public boolean equals(Object other) { 520 if (other == null) return false; 521 522 if (other instanceof DnsName) { 523 DnsName otherDnsName = (DnsName) other; 524 setBytesIfRequired(); 525 otherDnsName.setBytesIfRequired(); 526 return Arrays.equals(bytes, otherDnsName.bytes); 527 } 528 529 return false; 530 } 531 532 @Override 533 public int hashCode() { 534 if (hashCode == 0 && !isRootLabel()) { 535 setBytesIfRequired(); 536 hashCode = Arrays.hashCode(bytes); 537 } 538 return hashCode; 539 } 540 541 public boolean isDirectChildOf(DnsName parent) { 542 setLabelsIfRequired(); 543 parent.setLabelsIfRequired(); 544 int parentLabelsCount = parent.labels.length; 545 546 if (labels.length - 1 != parentLabelsCount) 547 return false; 548 549 for (int i = 0; i < parent.labels.length; i++) { 550 if (!labels[i].equals(parent.labels[i])) 551 return false; 552 } 553 554 return true; 555 } 556 557 public boolean isChildOf(DnsName parent) { 558 setLabelsIfRequired(); 559 parent.setLabelsIfRequired(); 560 561 if (labels.length < parent.labels.length) 562 return false; 563 564 for (int i = 0; i < parent.labels.length; i++) { 565 if (!labels[i].equals(parent.labels[i])) 566 return false; 567 } 568 569 return true; 570 } 571 572 /** 573 * Replace a suffix of this DNS name. 574 * <p> 575 * For example, replacing the suffix "example.org" of "foo.example.org" with "example.net" results in 576 * "foo.example.net". This is the substitution performed for DNAME RRs (RFC 6672 § 2.2). 577 * </p> 578 * 579 * @param oldSuffix the suffix to replace. This DNS name must be a child of, or equal to, it. 580 * @param newSuffix the new suffix. 581 * @return the resulting DNS name. 582 * @throws IllegalArgumentException if this DNS name is not a child of, or equal to, oldSuffix. 583 * @throws InvalidDnsNameException.DNSNameTooLongException if the resulting DNS name is too long. 584 */ 585 public DnsName replaceSuffix(DnsName oldSuffix, DnsName newSuffix) { 586 if (!isChildOf(oldSuffix)) { 587 throw new IllegalArgumentException(this + " is not a child of " + oldSuffix); 588 } 589 590 int oldSuffixLabelCount = oldSuffix.labels.length; 591 int prefixLabelCount = rawLabels.length - oldSuffixLabelCount; 592 if (prefixLabelCount == 0) { 593 return newSuffix; 594 } 595 596 newSuffix.setLabelsIfRequired(); 597 DnsLabel[] resultRawLabels = new DnsLabel[newSuffix.rawLabels.length + prefixLabelCount]; 598 System.arraycopy(newSuffix.rawLabels, 0, resultRawLabels, 0, newSuffix.rawLabels.length); 599 System.arraycopy(rawLabels, oldSuffixLabelCount, resultRawLabels, newSuffix.rawLabels.length, prefixLabelCount); 600 return new DnsName(resultRawLabels, true); 601 } 602 603 public int getLabelCount() { 604 setLabelsIfRequired(); 605 return labels.length; 606 } 607 608 /** 609 * Get a copy of the labels of this DNS name. The resulting array will contain the labels in reverse order, that is, 610 * the top-level domain will be at res[0]. 611 * 612 * @return an array of the labels in reverse order. 613 */ 614 public DnsLabel[] getLabels() { 615 setLabelsIfRequired(); 616 return labels.clone(); 617 } 618 619 620 public DnsLabel getLabel(int labelNum) { 621 setLabelsIfRequired(); 622 return labels[labelNum]; 623 } 624 625 /** 626 * Get a copy of the raw labels of this DNS name. The resulting array will contain the labels in reverse order, that is, 627 * the top-level domain will be at res[0]. 628 * 629 * @return an array of the raw labels in reverse order. 630 */ 631 public DnsLabel[] getRawLabels() { 632 setLabelsIfRequired(); 633 return rawLabels.clone(); 634 } 635 636 public DnsName stripToLabels(int labelCount) { 637 setLabelsIfRequired(); 638 639 if (labelCount > labels.length) { 640 throw new IllegalArgumentException(); 641 } 642 643 if (labelCount == labels.length) { 644 return this; 645 } 646 647 if (labelCount == 0) { 648 return ROOT; 649 } 650 651 DnsLabel[] stripedLabels = Arrays.copyOfRange(rawLabels, 0, labelCount); 652 653 return new DnsName(stripedLabels, false); 654 } 655 656 /** 657 * 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). 658 * <p> 659 * For example: 660 * </p> 661 * <ul> 662 * <li><code>"foo.bar.org".getParent() == "bar.org"</code></li> 663 * <li><code> ".".getParent() == "."</code></li> 664 * </ul> 665 * @return the parent of this DNS label. 666 */ 667 public DnsName getParent() { 668 if (isRootLabel()) return ROOT; 669 return stripToLabels(getLabelCount() - 1); 670 } 671 672 public boolean isRootLabel() { 673 return ace.isEmpty() || ace.equals("."); 674 } 675}