/* | 
|
 * Copyright (c) 2012, 2018, 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. | 
|
 * <P> | 
|
 * As described in section 3, "Server Name Indication", of | 
|
 * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">TLS Extensions (RFC 6066)</A>, | 
|
 * "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 <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A>. | 
|
 * <P> | 
|
 * 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. | 
|
     * <P> | 
|
     * Note that per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, | 
|
     * 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 | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>, | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>, | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>) and | 
|
     * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as: | 
|
     * <pre> | 
|
     *     IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES); | 
|
     * </pre> | 
|
     * <P> | 
|
     * The {@code hostname} argument is illegal if it: | 
|
     * <ul> | 
|
     * <li> {@code hostname} is empty,</li> | 
|
     * <li> {@code hostname} ends with a trailing dot,</li> | 
|
     * <li> {@code hostname} is not a valid Internationalized | 
|
     *      Domain Name (IDN) compliant with the RFC 3490 specification.</li> | 
|
     * </ul> | 
|
     * @param  hostname | 
|
     *         the hostname of this server name | 
|
     * | 
|
     * @throws NullPointerException if {@code hostname} is {@code null} | 
|
     * @throws IllegalArgumentException if {@code hostname} is illegal | 
|
*/  | 
|
public SNIHostName(String hostname) {  | 
|
        // IllegalArgumentException will be thrown if {@code hostname} is | 
|
        // not a valid IDN. | 
|
super(StandardConstants.SNI_HOST_NAME,  | 
|
(hostname = IDN.toASCII(  | 
|
Objects.requireNonNull(hostname,  | 
|
                        "Server name value of host_name cannot be null"), | 
|
IDN.USE_STD3_ASCII_RULES))  | 
|
.getBytes(StandardCharsets.US_ASCII));  | 
|
this.hostname = hostname;  | 
|
        // check the validity of the string hostname | 
|
checkHostName();  | 
|
}  | 
|
    /** | 
|
     * Creates an {@code SNIHostName} using the specified encoded value. | 
|
     * <P> | 
|
     * This method is normally used to parse the encoded name value in a | 
|
     * requested SNI extension. | 
|
     * <P> | 
|
     * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, | 
|
     * the encoded name value of a hostname is | 
|
     * {@link StandardCharsets#US_ASCII}-compliant.  However, in the previous | 
|
     * version of the SNI extension ( | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc4366.txt">RFC 4366</A>), | 
|
     * 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. | 
|
     * <P> | 
|
     * 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 <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, | 
|
     * please always use {@link StandardCharsets#US_ASCII}-compliant charset | 
|
     * and enforce the restrictions on ASCII characters in hostnames (see | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>, | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>, | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>) | 
|
     * for {@code encoded} argument, or use | 
|
     * {@link SNIHostName#SNIHostName(String)} instead. | 
|
     * <P> | 
|
     * The {@code encoded} argument is illegal if it: | 
|
     * <ul> | 
|
     * <li> {@code encoded} is empty,</li> | 
|
     * <li> {@code encoded} ends with a trailing dot,</li> | 
|
     * <li> {@code encoded} is not encoded in | 
|
     *      {@link StandardCharsets#US_ASCII} or | 
|
     *      {@link StandardCharsets#UTF_8}-compliant charset,</li> | 
|
     * <li> {@code encoded} is not a valid Internationalized | 
|
     *      Domain Name (IDN) compliant with the RFC 3490 specification.</li> | 
|
     * </ul> | 
|
     * | 
|
     * <P> | 
|
     * 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. | 
|
     * <P> | 
|
     * Note that, per | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, the | 
|
     * returned hostname may be an internationalized domain name that | 
|
     * contains A-labels. See | 
|
     * <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A> | 
|
     * 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. | 
|
     * <P> | 
|
     * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, 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}. | 
|
     * <P> | 
|
     * 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. | 
|
     * <P> | 
|
     * The exact details of the representation are unspecified and subject | 
|
     * to change, but the following may be regarded as typical: | 
|
     * <pre> | 
|
     *     "type=host_name (0), value={@literal <hostname>}" | 
|
     * </pre> | 
|
     * The "{@literal <hostname>}" is an ASCII representation of the hostname, | 
|
     * which may contains A-labels.  For example, a returned value of an pseudo | 
|
     * hostname may look like: | 
|
     * <pre> | 
|
     *     "type=host_name (0), value=www.example.com" | 
|
     * </pre> | 
|
     * or | 
|
     * <pre> | 
|
     *     "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d" | 
|
     * </pre> | 
|
     * <P> | 
|
     * 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. | 
|
     * <P> | 
|
     * This method can be used by a server to verify the acceptable | 
|
     * {@code SNIHostName}s.  For example, | 
|
     * <pre> | 
|
     *     SNIMatcher matcher = | 
|
     *         SNIHostName.createSNIMatcher("www\\.example\\.com"); | 
|
     * </pre> | 
|
     * will accept the hostname "www.example.com". | 
|
     * <pre> | 
|
     *     SNIMatcher matcher = | 
|
     *         SNIHostName.createSNIMatcher("www\\.example\\.(com|org)"); | 
|
     * </pre> | 
|
     * will accept hostnames "www.example.com" and "www.example.org". | 
|
     * | 
|
     * @param  regex | 
|
     *         the <a href="{@docRoot}/java.base/java/util/regex/Pattern.html#sum"> | 
|
     *         regular expression pattern</a> | 
|
     *         representing the hostname(s) to match | 
|
     * @return a {@code SNIMatcher} object for {@code SNIHostName}s | 
|
     * @throws NullPointerException if {@code regex} is | 
|
     *         {@code null} | 
|
     * @throws java.util.regex.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 static final class SNIHostNameMatcher extends SNIMatcher {  | 
|
        // the compiled representation of a regular expression. | 
|
private final Pattern pattern;  | 
|
        /** | 
|
         * Creates an SNIHostNameMatcher object. | 
|
         * | 
|
         * @param  regex | 
|
         *         the <a href="{@docRoot}/java.base/java/util/regex/Pattern.html#sum"> | 
|
         *         regular expression pattern</a> | 
|
         *         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();  | 
|
}  | 
|
}  | 
|
}  |