| /* |
| * Copyright (c) 2012, 2013, 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/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/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(); |
| } |
| } |
| } |