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.dnslabel; 012 013import java.io.ByteArrayOutputStream; 014import java.nio.charset.StandardCharsets; 015import java.util.Locale; 016 017import org.minidns.util.SafeCharSequence; 018 019/** 020 * A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots. 021 * <p> 022 * This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as 023 * specified in <a href="https://tools.ietf.org/html/rfc4034#section-6.1">RFC 4034 § 6.1</a>. 024 * </p> 025 * <p> 026 * Note that as per <a href="https://tools.ietf.org/html/rfc2181#section-11">RFC 2181 § 11</a> DNS labels may contain 027 * any byte. 028 * </p> 029 * 030 * @see <a href="https://tools.ietf.org/html/rfc5890#section-2.2">RFC 5890 § 2.2. DNS-Related Terminology</a> 031 * @author Florian Schmaus 032 * 033 */ 034public abstract class DnsLabel extends SafeCharSequence implements Comparable<DnsLabel> { 035 036 /** 037 * The maximum length of a DNS label in octets. 038 * 039 * @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a> 040 */ 041 public static final int MAX_LABEL_LENGTH_IN_OCTETS = 63; 042 043 public static final DnsLabel WILDCARD_LABEL = DnsLabel.from("*"); 044 045 /** 046 * Whether or not the DNS label is validated on construction. 047 */ 048 public static boolean VALIDATE = true; 049 050 public final String label; 051 052 protected DnsLabel(String label) { 053 this.label = label; 054 055 if (!VALIDATE) { 056 return; 057 } 058 059 setBytesIfRequired(); 060 if (byteCache.length > MAX_LABEL_LENGTH_IN_OCTETS) { 061 throw new LabelToLongException(label); 062 } 063 } 064 065 private transient String internationalizedRepresentation; 066 067 public final String getInternationalizedRepresentation() { 068 if (internationalizedRepresentation == null) { 069 internationalizedRepresentation = getInternationalizedRepresentationInternal(); 070 } 071 return internationalizedRepresentation; 072 } 073 074 protected String getInternationalizedRepresentationInternal() { 075 return label; 076 } 077 078 public final String getLabelType() { 079 return getClass().getSimpleName(); 080 } 081 082 private transient String safeToStringRepresentation; 083 084 @Override 085 public final String toString() { 086 if (safeToStringRepresentation == null) { 087 safeToStringRepresentation = toSafeRepesentation(label); 088 } 089 090 return safeToStringRepresentation; 091 } 092 093 /** 094 * Get the raw label. Note that this may return a String containing null bytes. 095 * Those Strings are notoriously difficult to handle from a security 096 * perspective. Therefore it is recommended to use {@link #toString()} instead, 097 * which will return a sanitized String. 098 * 099 * @return the raw label. 100 * @since 1.1.0 101 */ 102 public final String getRawLabel() { 103 return label; 104 } 105 106 @Override 107 public final boolean equals(Object other) { 108 if (!(other instanceof DnsLabel)) { 109 return false; 110 } 111 DnsLabel otherDnsLabel = (DnsLabel) other; 112 return label.equals(otherDnsLabel.label); 113 } 114 115 @Override 116 public final int hashCode() { 117 return label.hashCode(); 118 } 119 120 private transient DnsLabel lowercasedVariant; 121 122 public final DnsLabel asLowercaseVariant() { 123 if (lowercasedVariant == null) { 124 String lowercaseLabel = label.toLowerCase(Locale.US); 125 lowercasedVariant = DnsLabel.from(lowercaseLabel); 126 } 127 return lowercasedVariant; 128 } 129 130 private transient byte[] byteCache; 131 132 private void setBytesIfRequired() { 133 if (byteCache == null) { 134 byteCache = label.getBytes(StandardCharsets.US_ASCII); 135 } 136 } 137 138 public final void writeToBoas(ByteArrayOutputStream byteArrayOutputStream) { 139 setBytesIfRequired(); 140 141 byteArrayOutputStream.write(byteCache.length); 142 byteArrayOutputStream.write(byteCache, 0, byteCache.length); 143 } 144 145 @Override 146 public final int compareTo(DnsLabel other) { 147 String myCanonical = asLowercaseVariant().label; 148 String otherCanonical = other.asLowercaseVariant().label; 149 150 return myCanonical.compareTo(otherCanonical); 151 } 152 153 public static DnsLabel from(String label) { 154 if (label == null || label.isEmpty()) { 155 throw new IllegalArgumentException("Label is null or empty"); 156 } 157 158 if (LdhLabel.isLdhLabel(label)) { 159 return LdhLabel.fromInternal(label); 160 } 161 162 return NonLdhLabel.fromInternal(label); 163 } 164 165 public static DnsLabel[] from(String[] labels) { 166 DnsLabel[] res = new DnsLabel[labels.length]; 167 168 for (int i = 0; i < labels.length; i++) { 169 res[i] = DnsLabel.from(labels[i]); 170 } 171 172 return res; 173 } 174 175 public static boolean isIdnAcePrefixed(String string) { 176 return string.toLowerCase(Locale.US).startsWith("xn--"); 177 } 178 179 public static String toSafeRepesentation(String dnsLabel) { 180 if (consistsOnlyOfLettersDigitsHypenAndUnderscore(dnsLabel)) { 181 // This label is safe, nothing to do. 182 return dnsLabel; 183 } 184 185 StringBuilder sb = new StringBuilder(2 * dnsLabel.length()); 186 for (int i = 0; i < dnsLabel.length(); i++) { 187 char c = dnsLabel.charAt(i); 188 if (isLdhOrMaybeUnderscore(c, true)) { 189 sb.append(c); 190 continue; 191 } 192 193 194 // Let's see if we found and unsafe char we want to replace. 195 switch (c) { 196 case '.': 197 sb.append('●'); // U+25CF BLACK CIRCLE; 198 break; 199 case '\\': 200 sb.append('⧷'); // U+29F7 REVERSE SOLIDUS WITH HORIZONTAL STROKE 201 break; 202 case '\u007f': 203 // Convert DEL to U+2421 SYMBOL FOR DELETE 204 sb.append('␡'); 205 break; 206 case ' ': 207 sb.append('␣'); // U+2423 OPEN BOX 208 break; 209 default: 210 if (c < 32) { 211 // First convert the ASCI control codes to the Unicode Control Pictures 212 int substituteAsInt = c + '\u2400'; 213 assert substituteAsInt <= Character.MAX_CODE_POINT; 214 char substitute = (char) substituteAsInt; 215 sb.append(substitute); 216 } else if (c < 127) { 217 // Everything smaller than 127 is now safe to directly append. 218 sb.append(c); 219 } else if (c > 255) { 220 throw new IllegalArgumentException("The string '" + dnsLabel 221 + "' contains characters outside the 8-bit range: " + c + " at position " + i); 222 } else { 223 // Everything that did not match the previous conditions is explicitly escaped. 224 sb.append("〚"); // U+301A 225 // Transform the char to hex notation. Note that we have ensure that c is <= 255 226 // here, hence only two hexadecimal places are ok. 227 String hex = String.format("%02X", (int) c); 228 sb.append(hex); 229 sb.append("〛"); // U+301B 230 } 231 } 232 } 233 234 return sb.toString(); 235 } 236 237 private static boolean isLdhOrMaybeUnderscore(char c, boolean underscore) { 238 return (c >= 'a' && c <= 'z') 239 || (c >= 'A' && c <= 'Z') 240 || (c >= '0' && c <= '9') 241 || c == '-' 242 || (underscore && c == '_') 243 ; 244 } 245 246 private static boolean consistsOnlyOfLdhAndMaybeUnderscore(String string, boolean underscore) { 247 for (int i = 0; i < string.length(); i++) { 248 char c = string.charAt(i); 249 if (isLdhOrMaybeUnderscore(c, underscore)) { 250 continue; 251 } 252 return false; 253 } 254 return true; 255 } 256 257 public static boolean consistsOnlyOfLettersDigitsAndHypen(String string) { 258 return consistsOnlyOfLdhAndMaybeUnderscore(string, false); 259 } 260 261 public static boolean consistsOnlyOfLettersDigitsHypenAndUnderscore(String string) { 262 return consistsOnlyOfLdhAndMaybeUnderscore(string, true); 263 } 264 265 public static class LabelToLongException extends IllegalArgumentException { 266 267 /** 268 * 269 */ 270 private static final long serialVersionUID = 1L; 271 272 public final String label; 273 274 LabelToLongException(String label) { 275 this.label = label; 276 } 277 } 278}