diff options
Diffstat (limited to 'ojluni/src/main/java/javax/net/ssl/SNIHostName.java')
-rw-r--r-- | ojluni/src/main/java/javax/net/ssl/SNIHostName.java | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/ojluni/src/main/java/javax/net/ssl/SNIHostName.java b/ojluni/src/main/java/javax/net/ssl/SNIHostName.java new file mode 100644 index 00000000000..d5e71414497 --- /dev/null +++ b/ojluni/src/main/java/javax/net/ssl/SNIHostName.java @@ -0,0 +1,396 @@ +/* + * 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 final static 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(); + } + } +} |