1 /* 2 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javax.net.ssl; 27 28 import java.net.IDN; 29 import java.nio.ByteBuffer; 30 import java.nio.charset.CodingErrorAction; 31 import java.nio.charset.StandardCharsets; 32 import java.nio.charset.CharsetDecoder; 33 import java.nio.charset.CharacterCodingException; 34 import java.util.Locale; 35 import java.util.Objects; 36 import java.util.regex.Pattern; 37 38 /** 39 * Instances of this class represent a server name of type 40 * {@link StandardConstants#SNI_HOST_NAME host_name} in a Server Name 41 * Indication (SNI) extension. 42 * <P> 43 * As described in section 3, "Server Name Indication", of 44 * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">TLS Extensions (RFC 6066)</A>, 45 * "HostName" contains the fully qualified DNS hostname of the server, as 46 * understood by the client. The encoded server name value of a hostname is 47 * represented as a byte string using ASCII encoding without a trailing dot. 48 * This allows the support of Internationalized Domain Names (IDN) through 49 * the use of A-labels (the ASCII-Compatible Encoding (ACE) form of a valid 50 * string of Internationalized Domain Names for Applications (IDNA)) defined 51 * in <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A>. 52 * <P> 53 * Note that {@code SNIHostName} objects are immutable. 54 * 55 * @see SNIServerName 56 * @see StandardConstants#SNI_HOST_NAME 57 * 58 * @since 1.8 59 */ 60 public final class SNIHostName extends SNIServerName { 61 62 // the decoded string value of the server name 63 private final String hostname; 64 65 /** 66 * Creates an {@code SNIHostName} using the specified hostname. 67 * <P> 68 * Note that per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, 69 * the encoded server name value of a hostname is 70 * {@link StandardCharsets#US_ASCII}-compliant. In this method, 71 * {@code hostname} can be a user-friendly Internationalized Domain Name 72 * (IDN). {@link IDN#toASCII(String, int)} is used to enforce the 73 * restrictions on ASCII characters in hostnames (see 74 * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>, 75 * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>, 76 * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>) and 77 * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as: 78 * <pre> 79 * IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES); 80 * </pre> 81 * <P> 82 * The {@code hostname} argument is illegal if it: 83 * <ul> 84 * <li> {@code hostname} is empty,</li> 85 * <li> {@code hostname} ends with a trailing dot,</li> 86 * <li> {@code hostname} is not a valid Internationalized 87 * Domain Name (IDN) compliant with the RFC 3490 specification.</li> 88 * </ul> 89 * @param hostname 90 * the hostname of this server name 91 * 92 * @throws NullPointerException if {@code hostname} is {@code null} 93 * @throws IllegalArgumentException if {@code hostname} is illegal 94 */ SNIHostName(String hostname)95 public SNIHostName(String hostname) { 96 // IllegalArgumentException will be thrown if {@code hostname} is 97 // not a valid IDN. 98 super(StandardConstants.SNI_HOST_NAME, 99 (hostname = IDN.toASCII( 100 Objects.requireNonNull(hostname, 101 "Server name value of host_name cannot be null"), 102 IDN.USE_STD3_ASCII_RULES)) 103 .getBytes(StandardCharsets.US_ASCII)); 104 105 this.hostname = hostname; 106 107 // check the validity of the string hostname 108 checkHostName(); 109 } 110 111 /** 112 * Creates an {@code SNIHostName} using the specified encoded value. 113 * <P> 114 * This method is normally used to parse the encoded name value in a 115 * requested SNI extension. 116 * <P> 117 * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, 118 * the encoded name value of a hostname is 119 * {@link StandardCharsets#US_ASCII}-compliant. However, in the previous 120 * version of the SNI extension ( 121 * <A HREF="http://www.ietf.org/rfc/rfc4366.txt">RFC 4366</A>), 122 * the encoded hostname is represented as a byte string using UTF-8 123 * encoding. For the purpose of version tolerance, this method allows 124 * that the charset of {@code encoded} argument can be 125 * {@link StandardCharsets#UTF_8}, as well as 126 * {@link StandardCharsets#US_ASCII}. {@link IDN#toASCII(String)} is used 127 * to translate the {@code encoded} argument into ASCII Compatible 128 * Encoding (ACE) hostname. 129 * <P> 130 * It is strongly recommended that this constructor is only used to parse 131 * the encoded name value in a requested SNI extension. Otherwise, to 132 * comply with <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, 133 * please always use {@link StandardCharsets#US_ASCII}-compliant charset 134 * and enforce the restrictions on ASCII characters in hostnames (see 135 * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>, 136 * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>, 137 * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>) 138 * for {@code encoded} argument, or use 139 * {@link SNIHostName#SNIHostName(String)} instead. 140 * <P> 141 * The {@code encoded} argument is illegal if it: 142 * <ul> 143 * <li> {@code encoded} is empty,</li> 144 * <li> {@code encoded} ends with a trailing dot,</li> 145 * <li> {@code encoded} is not encoded in 146 * {@link StandardCharsets#US_ASCII} or 147 * {@link StandardCharsets#UTF_8}-compliant charset,</li> 148 * <li> {@code encoded} is not a valid Internationalized 149 * Domain Name (IDN) compliant with the RFC 3490 specification.</li> 150 * </ul> 151 * 152 * <P> 153 * Note that the {@code encoded} byte array is cloned 154 * to protect against subsequent modification. 155 * 156 * @param encoded 157 * the encoded hostname of this server name 158 * 159 * @throws NullPointerException if {@code encoded} is {@code null} 160 * @throws IllegalArgumentException if {@code encoded} is illegal 161 */ SNIHostName(byte[] encoded)162 public SNIHostName(byte[] encoded) { 163 // NullPointerException will be thrown if {@code encoded} is null 164 super(StandardConstants.SNI_HOST_NAME, encoded); 165 166 // Compliance: RFC 4366 requires that the hostname is represented 167 // as a byte string using UTF_8 encoding [UTF8] 168 try { 169 // Please don't use {@link String} constructors because they 170 // do not report coding errors. 171 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() 172 .onMalformedInput(CodingErrorAction.REPORT) 173 .onUnmappableCharacter(CodingErrorAction.REPORT); 174 175 this.hostname = IDN.toASCII( 176 decoder.decode(ByteBuffer.wrap(encoded)).toString()); 177 } catch (RuntimeException | CharacterCodingException e) { 178 throw new IllegalArgumentException( 179 "The encoded server name value is invalid", e); 180 } 181 182 // check the validity of the string hostname 183 checkHostName(); 184 } 185 186 /** 187 * Returns the {@link StandardCharsets#US_ASCII}-compliant hostname of 188 * this {@code SNIHostName} object. 189 * <P> 190 * Note that, per 191 * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, the 192 * returned hostname may be an internationalized domain name that 193 * contains A-labels. See 194 * <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A> 195 * for more information about the detailed A-label specification. 196 * 197 * @return the {@link StandardCharsets#US_ASCII}-compliant hostname 198 * of this {@code SNIHostName} object 199 */ getAsciiName()200 public String getAsciiName() { 201 return hostname; 202 } 203 204 /** 205 * Compares this server name to the specified object. 206 * <P> 207 * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, DNS 208 * hostnames are case-insensitive. Two server hostnames are equal if, 209 * and only if, they have the same name type, and the hostnames are 210 * equal in a case-independent comparison. 211 * 212 * @param other 213 * the other server name object to compare with. 214 * @return true if, and only if, the {@code other} is considered 215 * equal to this instance 216 */ 217 @Override equals(Object other)218 public boolean equals(Object other) { 219 if (this == other) { 220 return true; 221 } 222 223 if (other instanceof SNIHostName) { 224 return hostname.equalsIgnoreCase(((SNIHostName)other).hostname); 225 } 226 227 return false; 228 } 229 230 /** 231 * Returns a hash code value for this {@code SNIHostName}. 232 * <P> 233 * The hash code value is generated using the case-insensitive hostname 234 * of this {@code SNIHostName}. 235 * 236 * @return a hash code value for this {@code SNIHostName}. 237 */ 238 @Override hashCode()239 public int hashCode() { 240 int result = 17; // 17/31: prime number to decrease collisions 241 result = 31 * result + hostname.toUpperCase(Locale.ENGLISH).hashCode(); 242 243 return result; 244 } 245 246 /** 247 * Returns a string representation of the object, including the DNS 248 * hostname in this {@code SNIHostName} object. 249 * <P> 250 * The exact details of the representation are unspecified and subject 251 * to change, but the following may be regarded as typical: 252 * <pre> 253 * "type=host_name (0), value={@literal <hostname>}" 254 * </pre> 255 * The "{@literal <hostname>}" is an ASCII representation of the hostname, 256 * which may contains A-labels. For example, a returned value of an pseudo 257 * hostname may look like: 258 * <pre> 259 * "type=host_name (0), value=www.example.com" 260 * </pre> 261 * or 262 * <pre> 263 * "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d" 264 * </pre> 265 * <P> 266 * Please NOTE that the exact details of the representation are unspecified 267 * and subject to change. 268 * 269 * @return a string representation of the object. 270 */ 271 @Override toString()272 public String toString() { 273 return "type=host_name (0), value=" + hostname; 274 } 275 276 /** 277 * Creates an {@link SNIMatcher} object for {@code SNIHostName}s. 278 * <P> 279 * This method can be used by a server to verify the acceptable 280 * {@code SNIHostName}s. For example, 281 * <pre> 282 * SNIMatcher matcher = 283 * SNIHostName.createSNIMatcher("www\\.example\\.com"); 284 * </pre> 285 * will accept the hostname "www.example.com". 286 * <pre> 287 * SNIMatcher matcher = 288 * SNIHostName.createSNIMatcher("www\\.example\\.(com|org)"); 289 * </pre> 290 * will accept hostnames "www.example.com" and "www.example.org". 291 * 292 * @param regex 293 * the <a href="{@docRoot}/java/util/regex/Pattern.html#sum"> 294 * regular expression pattern</a> 295 * representing the hostname(s) to match 296 * @return a {@code SNIMatcher} object for {@code SNIHostName}s 297 * @throws NullPointerException if {@code regex} is 298 * {@code null} 299 * @throws java.util.regex.PatternSyntaxException if the regular expression's 300 * syntax is invalid 301 */ createSNIMatcher(String regex)302 public static SNIMatcher createSNIMatcher(String regex) { 303 if (regex == null) { 304 throw new NullPointerException( 305 "The regular expression cannot be null"); 306 } 307 308 return new SNIHostNameMatcher(regex); 309 } 310 311 // check the validity of the string hostname checkHostName()312 private void checkHostName() { 313 if (hostname.isEmpty()) { 314 throw new IllegalArgumentException( 315 "Server name value of host_name cannot be empty"); 316 } 317 318 if (hostname.endsWith(".")) { 319 throw new IllegalArgumentException( 320 "Server name value of host_name cannot have the trailing dot"); 321 } 322 } 323 324 private final static class SNIHostNameMatcher extends SNIMatcher { 325 326 // the compiled representation of a regular expression. 327 private final Pattern pattern; 328 329 /** 330 * Creates an SNIHostNameMatcher object. 331 * 332 * @param regex 333 * the <a href="{@docRoot}/java/util/regex/Pattern.html#sum"> 334 * regular expression pattern</a> 335 * representing the hostname(s) to match 336 * @throws NullPointerException if {@code regex} is 337 * {@code null} 338 * @throws PatternSyntaxException if the regular expression's syntax 339 * is invalid 340 */ SNIHostNameMatcher(String regex)341 SNIHostNameMatcher(String regex) { 342 super(StandardConstants.SNI_HOST_NAME); 343 pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); 344 } 345 346 /** 347 * Attempts to match the given {@link SNIServerName}. 348 * 349 * @param serverName 350 * the {@link SNIServerName} instance on which this matcher 351 * performs match operations 352 * 353 * @return {@code true} if, and only if, the matcher matches the 354 * given {@code serverName} 355 * 356 * @throws NullPointerException if {@code serverName} is {@code null} 357 * @throws IllegalArgumentException if {@code serverName} is 358 * not of {@code StandardConstants#SNI_HOST_NAME} type 359 * 360 * @see SNIServerName 361 */ 362 @Override matches(SNIServerName serverName)363 public boolean matches(SNIServerName serverName) { 364 if (serverName == null) { 365 throw new NullPointerException( 366 "The SNIServerName argument cannot be null"); 367 } 368 369 SNIHostName hostname; 370 if (!(serverName instanceof SNIHostName)) { 371 if (serverName.getType() != StandardConstants.SNI_HOST_NAME) { 372 throw new IllegalArgumentException( 373 "The server name type is not host_name"); 374 } 375 376 try { 377 hostname = new SNIHostName(serverName.getEncoded()); 378 } catch (NullPointerException | IllegalArgumentException e) { 379 return false; 380 } 381 } else { 382 hostname = (SNIHostName)serverName; 383 } 384 385 // Let's first try the ascii name matching 386 String asciiName = hostname.getAsciiName(); 387 if (pattern.matcher(asciiName).matches()) { 388 return true; 389 } 390 391 // May be an internationalized domain name, check the Unicode 392 // representations. 393 return pattern.matcher(IDN.toUnicode(asciiName)).matches(); 394 } 395 } 396 } 397