1 /*
   2  * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.net.ssl;
  27 
  28 import java.net.IDN;
  29 import java.nio.ByteBuffer;
  30 import java.nio.charset.CodingErrorAction;
  31 import java.nio.charset.StandardCharsets;
  32 import java.nio.charset.CharsetDecoder;
  33 import java.nio.charset.CharacterCodingException;
  34 import java.util.Locale;
  35 import java.util.Objects;
  36 import java.util.regex.Pattern;
  37 
  38 /**
  39  * Instances of this class represent a server name of type
  40  * {@link StandardConstants#SNI_HOST_NAME host_name} in a Server Name
  41  * Indication (SNI) extension.
  42  * <P>
  43  * As described in section 3, "Server Name Indication", of
  44  * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">TLS Extensions (RFC 6066)</A>,
  45  * "HostName" contains the fully qualified DNS hostname of the server, as
  46  * understood by the client.  The encoded server name value of a hostname is
  47  * represented as a byte string using ASCII encoding without a trailing dot.
  48  * This allows the support of Internationalized Domain Names (IDN) through
  49  * the use of A-labels (the ASCII-Compatible Encoding (ACE) form of a valid
  50  * string of Internationalized Domain Names for Applications (IDNA)) defined
  51  * in <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A>.
  52  * <P>
  53  * Note that {@code SNIHostName} objects are immutable.
  54  *
  55  * @see SNIServerName
  56  * @see StandardConstants#SNI_HOST_NAME
  57  *
  58  * @since 1.8
  59  */
  60 public final class SNIHostName extends SNIServerName {
  61 
  62     // the decoded string value of the server name
  63     private final String hostname;
  64 
  65     /**
  66      * Creates an {@code SNIHostName} using the specified hostname.
  67      * <P>
  68      * Note that per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>,
  69      * the encoded server name value of a hostname is
  70      * {@link StandardCharsets#US_ASCII}-compliant.  In this method,
  71      * {@code hostname} can be a user-friendly Internationalized Domain Name
  72      * (IDN).  {@link IDN#toASCII(String, int)} is used to enforce the
  73      * restrictions on ASCII characters in hostnames (see
  74      * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>,
  75      * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>,
  76      * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>) and
  77      * translate the {@code hostname} into ASCII Compatible Encoding (ACE), as:
  78      * <pre>
  79      *     IDN.toASCII(hostname, IDN.USE_STD3_ASCII_RULES);
  80      * </pre>
  81      * <P>
  82      * The {@code hostname} argument is illegal if it:
  83      * <ul>
  84      * <li> {@code hostname} is empty,</li>
  85      * <li> {@code hostname} ends with a trailing dot,</li>
  86      * <li> {@code hostname} is not a valid Internationalized
  87      *      Domain Name (IDN) compliant with the RFC 3490 specification.</li>
  88      * </ul>
  89      * @param  hostname
  90      *         the hostname of this server name
  91      *
  92      * @throws NullPointerException if {@code hostname} is {@code null}
  93      * @throws IllegalArgumentException if {@code hostname} is illegal
  94      */
  95     public SNIHostName(String hostname) {
  96         // IllegalArgumentException will be thrown if {@code hostname} is
  97         // not a valid IDN.
  98         super(StandardConstants.SNI_HOST_NAME,
  99                 (hostname = IDN.toASCII(
 100                     Objects.requireNonNull(hostname,
 101                         "Server name value of host_name cannot be null"),
 102                     IDN.USE_STD3_ASCII_RULES))
 103                 .getBytes(StandardCharsets.US_ASCII));
 104 
 105         this.hostname = hostname;
 106 
 107         // check the validity of the string hostname
 108         checkHostName();
 109     }
 110 
 111     /**
 112      * Creates an {@code SNIHostName} using the specified encoded value.
 113      * <P>
 114      * This method is normally used to parse the encoded name value in a
 115      * requested SNI extension.
 116      * <P>
 117      * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>,
 118      * the encoded name value of a hostname is
 119      * {@link StandardCharsets#US_ASCII}-compliant.  However, in the previous
 120      * version of the SNI extension (
 121      * <A HREF="http://www.ietf.org/rfc/rfc4366.txt">RFC 4366</A>),
 122      * the encoded hostname is represented as a byte string using UTF-8
 123      * encoding.  For the purpose of version tolerance, this method allows
 124      * that the charset of {@code encoded} argument can be
 125      * {@link StandardCharsets#UTF_8}, as well as
 126      * {@link StandardCharsets#US_ASCII}.  {@link IDN#toASCII(String)} is used
 127      * to translate the {@code encoded} argument into ASCII Compatible
 128      * Encoding (ACE) hostname.
 129      * <P>
 130      * It is strongly recommended that this constructor is only used to parse
 131      * the encoded name value in a requested SNI extension.  Otherwise, to
 132      * comply with <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>,
 133      * please always use {@link StandardCharsets#US_ASCII}-compliant charset
 134      * and enforce the restrictions on ASCII characters in hostnames (see
 135      * <A HREF="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</A>,
 136      * <A HREF="http://www.ietf.org/rfc/rfc1122.txt">RFC 1122</A>,
 137      * <A HREF="http://www.ietf.org/rfc/rfc1123.txt">RFC 1123</A>)
 138      * for {@code encoded} argument, or use
 139      * {@link SNIHostName#SNIHostName(String)} instead.
 140      * <P>
 141      * The {@code encoded} argument is illegal if it:
 142      * <ul>
 143      * <li> {@code encoded} is empty,</li>
 144      * <li> {@code encoded} ends with a trailing dot,</li>
 145      * <li> {@code encoded} is not encoded in
 146      *      {@link StandardCharsets#US_ASCII} or
 147      *      {@link StandardCharsets#UTF_8}-compliant charset,</li>
 148      * <li> {@code encoded} is not a valid Internationalized
 149      *      Domain Name (IDN) compliant with the RFC 3490 specification.</li>
 150      * </ul>
 151      *
 152      * <P>
 153      * Note that the {@code encoded} byte array is cloned
 154      * to protect against subsequent modification.
 155      *
 156      * @param  encoded
 157      *         the encoded hostname of this server name
 158      *
 159      * @throws NullPointerException if {@code encoded} is {@code null}
 160      * @throws IllegalArgumentException if {@code encoded} is illegal
 161      */
 162     public SNIHostName(byte[] encoded) {
 163         // NullPointerException will be thrown if {@code encoded} is null
 164         super(StandardConstants.SNI_HOST_NAME, encoded);
 165 
 166         // Compliance: RFC 4366 requires that the hostname is represented
 167         // as a byte string using UTF_8 encoding [UTF8]
 168         try {
 169             // Please don't use {@link String} constructors because they
 170             // do not report coding errors.
 171             CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
 172                     .onMalformedInput(CodingErrorAction.REPORT)
 173                     .onUnmappableCharacter(CodingErrorAction.REPORT);
 174 
 175             this.hostname = IDN.toASCII(
 176                     decoder.decode(ByteBuffer.wrap(encoded)).toString());
 177         } catch (RuntimeException | CharacterCodingException e) {
 178             throw new IllegalArgumentException(
 179                         "The encoded server name value is invalid", e);
 180         }
 181 
 182         // check the validity of the string hostname
 183         checkHostName();
 184     }
 185 
 186     /**
 187      * Returns the {@link StandardCharsets#US_ASCII}-compliant hostname of
 188      * this {@code SNIHostName} object.
 189      * <P>
 190      * Note that, per
 191      * <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, the
 192      * returned hostname may be an internationalized domain name that
 193      * contains A-labels. See
 194      * <A HREF="http://www.ietf.org/rfc/rfc5890.txt">RFC 5890</A>
 195      * for more information about the detailed A-label specification.
 196      *
 197      * @return the {@link StandardCharsets#US_ASCII}-compliant hostname
 198      *         of this {@code SNIHostName} object
 199      */
 200     public String getAsciiName() {
 201         return hostname;
 202     }
 203 
 204     /**
 205      * Compares this server name to the specified object.
 206      * <P>
 207      * Per <A HREF="http://www.ietf.org/rfc/rfc6066.txt">RFC 6066</A>, DNS
 208      * hostnames are case-insensitive.  Two server hostnames are equal if,
 209      * and only if, they have the same name type, and the hostnames are
 210      * equal in a case-independent comparison.
 211      *
 212      * @param  other
 213      *         the other server name object to compare with.
 214      * @return true if, and only if, the {@code other} is considered
 215      *         equal to this instance
 216      */
 217     @Override
 218     public boolean equals(Object other) {
 219         if (this == other) {
 220             return true;
 221         }
 222 
 223         if (other instanceof SNIHostName) {
 224             return hostname.equalsIgnoreCase(((SNIHostName)other).hostname);
 225         }
 226 
 227         return false;
 228     }
 229 
 230     /**
 231      * Returns a hash code value for this {@code SNIHostName}.
 232      * <P>
 233      * The hash code value is generated using the case-insensitive hostname
 234      * of this {@code SNIHostName}.
 235      *
 236      * @return a hash code value for this {@code SNIHostName}.
 237      */
 238     @Override
 239     public int hashCode() {
 240         int result = 17;        // 17/31: prime number to decrease collisions
 241         result = 31 * result + hostname.toUpperCase(Locale.ENGLISH).hashCode();
 242 
 243         return result;
 244     }
 245 
 246     /**
 247      * Returns a string representation of the object, including the DNS
 248      * hostname in this {@code SNIHostName} object.
 249      * <P>
 250      * The exact details of the representation are unspecified and subject
 251      * to change, but the following may be regarded as typical:
 252      * <pre>
 253      *     "type=host_name (0), value={@literal <hostname>}"
 254      * </pre>
 255      * The "{@literal <hostname>}" is an ASCII representation of the hostname,
 256      * which may contains A-labels.  For example, a returned value of an pseudo
 257      * hostname may look like:
 258      * <pre>
 259      *     "type=host_name (0), value=www.example.com"
 260      * </pre>
 261      * or
 262      * <pre>
 263      *     "type=host_name (0), value=xn--fsqu00a.xn--0zwm56d"
 264      * </pre>
 265      * <P>
 266      * Please NOTE that the exact details of the representation are unspecified
 267      * and subject to change.
 268      *
 269      * @return a string representation of the object.
 270      */
 271     @Override
 272     public String toString() {
 273         return "type=host_name (0), value=" + hostname;
 274     }
 275 
 276     /**
 277      * Creates an {@link SNIMatcher} object for {@code SNIHostName}s.
 278      * <P>
 279      * This method can be used by a server to verify the acceptable
 280      * {@code SNIHostName}s.  For example,
 281      * <pre>
 282      *     SNIMatcher matcher =
 283      *         SNIHostName.createSNIMatcher("www\\.example\\.com");
 284      * </pre>
 285      * will accept the hostname "www.example.com".
 286      * <pre>
 287      *     SNIMatcher matcher =
 288      *         SNIHostName.createSNIMatcher("www\\.example\\.(com|org)");
 289      * </pre>
 290      * will accept hostnames "www.example.com" and "www.example.org".
 291      *
 292      * @param  regex
 293      *         the <a href="{@docRoot}/java/util/regex/Pattern.html#sum">
 294      *         regular expression pattern</a>
 295      *         representing the hostname(s) to match
 296      * @throws NullPointerException if {@code regex} is
 297      *         {@code null}
 298      * @throws PatternSyntaxException if the regular expression's syntax
 299      *         is invalid
 300      */
 301     public static SNIMatcher createSNIMatcher(String regex) {
 302         if (regex == null) {
 303             throw new NullPointerException(
 304                 "The regular expression cannot be null");
 305         }
 306 
 307         return new SNIHostNameMatcher(regex);
 308     }
 309 
 310     // check the validity of the string hostname
 311     private void checkHostName() {
 312         if (hostname.isEmpty()) {
 313             throw new IllegalArgumentException(
 314                 "Server name value of host_name cannot be empty");
 315         }
 316 
 317         if (hostname.endsWith(".")) {
 318             throw new IllegalArgumentException(
 319                 "Server name value of host_name cannot have the trailing dot");
 320         }
 321     }
 322 
 323     private final static class SNIHostNameMatcher extends SNIMatcher {
 324 
 325         // the compiled representation of a regular expression.
 326         private final Pattern pattern;
 327 
 328         /**
 329          * Creates an SNIHostNameMatcher object.
 330          *
 331          * @param  regex
 332          *         the <a href="{@docRoot}/java/util/regex/Pattern.html#sum">
 333          *         regular expression pattern</a>
 334          *         representing the hostname(s) to match
 335          * @throws NullPointerException if {@code regex} is
 336          *         {@code null}
 337          * @throws PatternSyntaxException if the regular expression's syntax
 338          *         is invalid
 339          */
 340         SNIHostNameMatcher(String regex) {
 341             super(StandardConstants.SNI_HOST_NAME);
 342             pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
 343         }
 344 
 345         /**
 346          * Attempts to match the given {@link SNIServerName}.
 347          *
 348          * @param  serverName
 349          *         the {@link SNIServerName} instance on which this matcher
 350          *         performs match operations
 351          *
 352          * @return {@code true} if, and only if, the matcher matches the
 353          *         given {@code serverName}
 354          *
 355          * @throws NullPointerException if {@code serverName} is {@code null}
 356          * @throws IllegalArgumentException if {@code serverName} is
 357          *         not of {@code StandardConstants#SNI_HOST_NAME} type
 358          *
 359          * @see SNIServerName
 360          */
 361         @Override
 362         public boolean matches(SNIServerName serverName) {
 363             if (serverName == null) {
 364                 throw new NullPointerException(
 365                     "The SNIServerName argument cannot be null");
 366             }
 367 
 368             SNIHostName hostname;
 369             if (!(serverName instanceof SNIHostName)) {
 370                 if (serverName.getType() != StandardConstants.SNI_HOST_NAME) {
 371                     throw new IllegalArgumentException(
 372                         "The server name type is not host_name");
 373                 }
 374 
 375                 try {
 376                     hostname = new SNIHostName(serverName.getEncoded());
 377                 } catch (NullPointerException | IllegalArgumentException e) {
 378                     return false;
 379                 }
 380             } else {
 381                 hostname = (SNIHostName)serverName;
 382             }
 383 
 384             // Let's first try the ascii name matching
 385             String asciiName = hostname.getAsciiName();
 386             if (pattern.matcher(asciiName).matches()) {
 387                 return true;
 388             }
 389 
 390             // May be an internationalized domain name, check the Unicode
 391             // representations.
 392             return pattern.matcher(IDN.toUnicode(asciiName)).matches();
 393         }
 394     }
 395 }