/*
 * 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();
        }
    }
}
