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
26package javax.net.ssl;
27
28import java.net.IDN;
29import java.nio.ByteBuffer;
30import java.nio.charset.CodingErrorAction;
31import java.nio.charset.StandardCharsets;
32import java.nio.charset.CharsetDecoder;
33import java.nio.charset.CharacterCodingException;
34import java.util.Locale;
35import java.util.Objects;
36import 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 */
60public 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     * @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/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