| /* | |
| * Copyright (c) 2012, 2024, 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; | |
| import java.util.regex.PatternSyntaxException; | |
| /** | |
| * 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. | |
| * | |
| * @spec https://www.rfc-editor.org/info/rfc5890 | |
| * RFC 5890: Internationalized Domain Names for Applications (IDNA): | |
| * Definitions and Document Framework | |
| * @spec https://www.rfc-editor.org/info/rfc6066 | |
| * RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions | |
| * @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 | |
| * | |
| * @spec https://www.rfc-editor.org/info/rfc1122 | |
| * RFC 1122: Requirements for Internet Hosts - Communication Layers | |
| * @spec https://www.rfc-editor.org/info/rfc1123 | |
| * RFC 1123: Requirements for Internet Hosts - Application and Support | |
| * @spec https://www.rfc-editor.org/info/rfc3490 | |
| * RFC 3490: Internationalizing Domain Names in Applications (IDNA) | |
| * @spec https://www.rfc-editor.org/info/rfc6066 | |
| * RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions | |
| */ | |
| 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 | |
| * | |
| * @spec https://www.rfc-editor.org/info/rfc1122 | |
| * RFC 1122: Requirements for Internet Hosts - Communication Layers | |
| * @spec https://www.rfc-editor.org/info/rfc1123 | |
| * RFC 1123: Requirements for Internet Hosts - Application and Support | |
| * @spec https://www.rfc-editor.org/info/rfc3490 | |
| * RFC 3490: Internationalizing Domain Names in Applications (IDNA) | |
| * @spec https://www.rfc-editor.org/info/rfc4366 | |
| * RFC 4366: Transport Layer Security (TLS) Extensions | |
| * @spec https://www.rfc-editor.org/info/rfc6066 | |
| * RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions | |
| */ | |
| 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(), | |
| IDN.USE_STD3_ASCII_RULES); | |
| } 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 | |
| * | |
| * @spec https://www.rfc-editor.org/info/rfc5890 | |
| * RFC 5890: Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework | |
| * @spec https://www.rfc-editor.org/info/rfc6066 | |
| * RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions | |
| */ | |
| 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 | |
| * | |
| * @spec https://www.rfc-editor.org/info/rfc6066 | |
| * RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions | |
| */ | |
| @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 contain A-labels. For example, a returned value of a 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 {@linkplain Pattern##sum regular expression pattern} | |
| * representing the hostname(s) to match | |
| * @return a {@code SNIMatcher} object for {@code SNIHostName}s | |
| * @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 static final class SNIHostNameMatcher extends SNIMatcher { | |
| // the compiled representation of a regular expression. | |
| private final Pattern pattern; | |
| /** | |
| * Creates an SNIHostNameMatcher object. | |
| * | |
| * @param regex the {@linkplain Pattern##sum 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(); | |
| } | |
| } | |
| } |