1 /*
2  * Copyright (c) 2012, 2013, 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      */
SNIHostName(String hostname)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      */
SNIHostName(byte[] encoded)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      */
getAsciiName()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
equals(Object other)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
hashCode()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
toString()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      * @return a {@code SNIMatcher} object for {@code SNIHostName}s
297      * @throws NullPointerException if {@code regex} is
298      *         {@code null}
299      * @throws java.util.regex.PatternSyntaxException if the regular expression's
300      *         syntax is invalid
301      */
createSNIMatcher(String regex)302     public static SNIMatcher createSNIMatcher(String regex) {
303         if (regex == null) {
304             throw new NullPointerException(
305                 "The regular expression cannot be null");
306         }
307 
308         return new SNIHostNameMatcher(regex);
309     }
310 
311     // check the validity of the string hostname
checkHostName()312     private void checkHostName() {
313         if (hostname.isEmpty()) {
314             throw new IllegalArgumentException(
315                 "Server name value of host_name cannot be empty");
316         }
317 
318         if (hostname.endsWith(".")) {
319             throw new IllegalArgumentException(
320                 "Server name value of host_name cannot have the trailing dot");
321         }
322     }
323 
324     private final static class SNIHostNameMatcher extends SNIMatcher {
325 
326         // the compiled representation of a regular expression.
327         private final Pattern pattern;
328 
329         /**
330          * Creates an SNIHostNameMatcher object.
331          *
332          * @param  regex
333          *         the <a href="{@docRoot}/java/util/regex/Pattern.html#sum">
334          *         regular expression pattern</a>
335          *         representing the hostname(s) to match
336          * @throws NullPointerException if {@code regex} is
337          *         {@code null}
338          * @throws PatternSyntaxException if the regular expression's syntax
339          *         is invalid
340          */
SNIHostNameMatcher(String regex)341         SNIHostNameMatcher(String regex) {
342             super(StandardConstants.SNI_HOST_NAME);
343             pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
344         }
345 
346         /**
347          * Attempts to match the given {@link SNIServerName}.
348          *
349          * @param  serverName
350          *         the {@link SNIServerName} instance on which this matcher
351          *         performs match operations
352          *
353          * @return {@code true} if, and only if, the matcher matches the
354          *         given {@code serverName}
355          *
356          * @throws NullPointerException if {@code serverName} is {@code null}
357          * @throws IllegalArgumentException if {@code serverName} is
358          *         not of {@code StandardConstants#SNI_HOST_NAME} type
359          *
360          * @see SNIServerName
361          */
362         @Override
matches(SNIServerName serverName)363         public boolean matches(SNIServerName serverName) {
364             if (serverName == null) {
365                 throw new NullPointerException(
366                     "The SNIServerName argument cannot be null");
367             }
368 
369             SNIHostName hostname;
370             if (!(serverName instanceof SNIHostName)) {
371                 if (serverName.getType() != StandardConstants.SNI_HOST_NAME) {
372                     throw new IllegalArgumentException(
373                         "The server name type is not host_name");
374                 }
375 
376                 try {
377                     hostname = new SNIHostName(serverName.getEncoded());
378                 } catch (NullPointerException | IllegalArgumentException e) {
379                     return false;
380                 }
381             } else {
382                 hostname = (SNIHostName)serverName;
383             }
384 
385             // Let's first try the ascii name matching
386             String asciiName = hostname.getAsciiName();
387             if (pattern.matcher(asciiName).matches()) {
388                 return true;
389             }
390 
391             // May be an internationalized domain name, check the Unicode
392             // representations.
393             return pattern.matcher(IDN.toUnicode(asciiName)).matches();
394         }
395     }
396 }
397