/* * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.net.ssl; import java.net.IDN; import java.nio.ByteBuffer; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharacterCodingException; import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; /** * Instances of this class represent a server name of type * {@link StandardConstants#SNI_HOST_NAME host_name} in a Server Name * Indication (SNI) extension. *
* As described in section 3, "Server Name Indication", of * TLS Extensions (RFC 6066), * "HostName" contains the fully qualified DNS hostname of the server, as * understood by the client. The encoded server name value of a hostname is * represented as a byte string using ASCII encoding without a trailing dot. * This allows the support of Internationalized Domain Names (IDN) through * the use of A-labels (the ASCII-Compatible Encoding (ACE) form of a valid * string of Internationalized Domain Names for Applications (IDNA)) defined * in RFC 5890. *
* Note that {@code SNIHostName} objects are immutable. * * @see SNIServerName * @see StandardConstants#SNI_HOST_NAME * * @since 1.8 */ public final class SNIHostName extends SNIServerName { // the decoded string value of the server name private final String hostname; /** * Creates an {@code SNIHostName} using the specified hostname. *
* Note that per RFC 6066, * the encoded server name value of a hostname is * {@link StandardCharsets#US_ASCII}-compliant. In this method, * {@code hostname} can be a user-friendly Internationalized Domain Name * (IDN). {@link IDN#toASCII(String, int)} is used to enforce the * restrictions on ASCII characters in hostnames (see * RFC 3490, * RFC 1122, * RFC 1123) and * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as: *
* IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES); **
* The {@code hostname} argument is illegal if it: *
* This method is normally used to parse the encoded name value in a * requested SNI extension. *
* Per RFC 6066, * the encoded name value of a hostname is * {@link StandardCharsets#US_ASCII}-compliant. However, in the previous * version of the SNI extension ( * RFC 4366), * the encoded hostname is represented as a byte string using UTF-8 * encoding. For the purpose of version tolerance, this method allows * that the charset of {@code encoded} argument can be * {@link StandardCharsets#UTF_8}, as well as * {@link StandardCharsets#US_ASCII}. {@link IDN#toASCII(String)} is used * to translate the {@code encoded} argument into ASCII Compatible * Encoding (ACE) hostname. *
* It is strongly recommended that this constructor is only used to parse * the encoded name value in a requested SNI extension. Otherwise, to * comply with RFC 6066, * please always use {@link StandardCharsets#US_ASCII}-compliant charset * and enforce the restrictions on ASCII characters in hostnames (see * RFC 3490, * RFC 1122, * RFC 1123) * for {@code encoded} argument, or use * {@link SNIHostName#SNIHostName(String)} instead. *
* The {@code encoded} argument is illegal if it: *
* Note that the {@code encoded} byte array is cloned * to protect against subsequent modification. * * @param encoded * the encoded hostname of this server name * * @throws NullPointerException if {@code encoded} is {@code null} * @throws IllegalArgumentException if {@code encoded} is illegal */ public SNIHostName(byte[] encoded) { // NullPointerException will be thrown if {@code encoded} is null super(StandardConstants.SNI_HOST_NAME, encoded); // Compliance: RFC 4366 requires that the hostname is represented // as a byte string using UTF_8 encoding [UTF8] try { // Please don't use {@link String} constructors because they // do not report coding errors. CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT); this.hostname = IDN.toASCII( decoder.decode(ByteBuffer.wrap(encoded)).toString()); } catch (RuntimeException | CharacterCodingException e) { throw new IllegalArgumentException( "The encoded server name value is invalid", e); } // check the validity of the string hostname checkHostName(); } /** * Returns the {@link StandardCharsets#US_ASCII}-compliant hostname of * this {@code SNIHostName} object. *
* Note that, per * RFC 6066, the * returned hostname may be an internationalized domain name that * contains A-labels. See * RFC 5890 * for more information about the detailed A-label specification. * * @return the {@link StandardCharsets#US_ASCII}-compliant hostname * of this {@code SNIHostName} object */ public String getAsciiName() { return hostname; } /** * Compares this server name to the specified object. *
* Per RFC 6066, DNS * hostnames are case-insensitive. Two server hostnames are equal if, * and only if, they have the same name type, and the hostnames are * equal in a case-independent comparison. * * @param other * the other server name object to compare with. * @return true if, and only if, the {@code other} is considered * equal to this instance */ @Override public boolean equals(Object other) { if (this == other) { return true; } if (other instanceof SNIHostName) { return hostname.equalsIgnoreCase(((SNIHostName)other).hostname); } return false; } /** * Returns a hash code value for this {@code SNIHostName}. *
* The hash code value is generated using the case-insensitive hostname * of this {@code SNIHostName}. * * @return a hash code value for this {@code SNIHostName}. */ @Override public int hashCode() { int result = 17; // 17/31: prime number to decrease collisions result = 31 * result + hostname.toUpperCase(Locale.ENGLISH).hashCode(); return result; } /** * Returns a string representation of the object, including the DNS * hostname in this {@code SNIHostName} object. *
* The exact details of the representation are unspecified and subject * to change, but the following may be regarded as typical: *
* "type=host_name (0), value={@literal* The "{@literal}" *
* "type=host_name (0), value=www.example.com" ** or *
* "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d" **
* Please NOTE that the exact details of the representation are unspecified * and subject to change. * * @return a string representation of the object. */ @Override public String toString() { return "type=host_name (0), value=" + hostname; } /** * Creates an {@link SNIMatcher} object for {@code SNIHostName}s. *
* This method can be used by a server to verify the acceptable * {@code SNIHostName}s. For example, *
* SNIMatcher matcher = * SNIHostName.createSNIMatcher("www\\.example\\.com"); ** will accept the hostname "www.example.com". *
* SNIMatcher matcher = * SNIHostName.createSNIMatcher("www\\.example\\.(com|org)"); ** will accept hostnames "www.example.com" and "www.example.org". * * @param regex * the * regular expression pattern * representing the hostname(s) to match * @throws NullPointerException if {@code regex} is * {@code null} * @throws PatternSyntaxException if the regular expression's syntax * is invalid */ public static SNIMatcher createSNIMatcher(String regex) { if (regex == null) { throw new NullPointerException( "The regular expression cannot be null"); } return new SNIHostNameMatcher(regex); } // check the validity of the string hostname private void checkHostName() { if (hostname.isEmpty()) { throw new IllegalArgumentException( "Server name value of host_name cannot be empty"); } if (hostname.endsWith(".")) { throw new IllegalArgumentException( "Server name value of host_name cannot have the trailing dot"); } } private final static class SNIHostNameMatcher extends SNIMatcher { // the compiled representation of a regular expression. private final Pattern pattern; /** * Creates an SNIHostNameMatcher object. * * @param regex * the * regular expression pattern * representing the hostname(s) to match * @throws NullPointerException if {@code regex} is * {@code null} * @throws PatternSyntaxException if the regular expression's syntax * is invalid */ SNIHostNameMatcher(String regex) { super(StandardConstants.SNI_HOST_NAME); pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); } /** * Attempts to match the given {@link SNIServerName}. * * @param serverName * the {@link SNIServerName} instance on which this matcher * performs match operations * * @return {@code true} if, and only if, the matcher matches the * given {@code serverName} * * @throws NullPointerException if {@code serverName} is {@code null} * @throws IllegalArgumentException if {@code serverName} is * not of {@code StandardConstants#SNI_HOST_NAME} type * * @see SNIServerName */ @Override public boolean matches(SNIServerName serverName) { if (serverName == null) { throw new NullPointerException( "The SNIServerName argument cannot be null"); } SNIHostName hostname; if (!(serverName instanceof SNIHostName)) { if (serverName.getType() != StandardConstants.SNI_HOST_NAME) { throw new IllegalArgumentException( "The server name type is not host_name"); } try { hostname = new SNIHostName(serverName.getEncoded()); } catch (NullPointerException | IllegalArgumentException e) { return false; } } else { hostname = (SNIHostName)serverName; } // Let's first try the ascii name matching String asciiName = hostname.getAsciiName(); if (pattern.matcher(asciiName).matches()) { return true; } // May be an internationalized domain name, check the Unicode // representations. return pattern.matcher(IDN.toUnicode(asciiName)).matches(); } } }