1 /*
2 * Copyright (c) 2012, 2018, 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.base/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 */
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
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 static final 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.base/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 */
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
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