/*
 * Copyright (c) 1997, 2014, 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 sun.security.x509;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import sun.security.util.*;

/**
 * This class implements the URIName as required by the GeneralNames
 * ASN.1 object.
 * <p>
 * [RFC5280] When the subjectAltName extension contains a URI, the name MUST be
 * stored in the uniformResourceIdentifier (an IA5String). The name MUST
 * be a non-relative URL, and MUST follow the URL syntax and encoding
 * rules specified in [RFC 3986].  The name must include both a scheme
 * (e.g., "http" or "ftp") and a scheme-specific-part.  The scheme-
 * specific-part must include a fully qualified domain name or IP
 * address as the host.
 * <p>
 * As specified in [RFC 3986], the scheme name is not case-sensitive
 * (e.g., "http" is equivalent to "HTTP").  The host part is also not
 * case-sensitive, but other components of the scheme-specific-part may
 * be case-sensitive. When comparing URIs, conforming implementations
 * MUST compare the scheme and host without regard to case, but assume
 * the remainder of the scheme-specific-part is case sensitive.
 * <p>
 * [RFC1738] In general, URLs are written as follows:
 * <pre>
 * <scheme>:<scheme-specific-part>
 * </pre>
 * A URL contains the name of the scheme being used (<scheme>) followed
 * by a colon and then a string (the <scheme-specific-part>) whose
 * interpretation depends on the scheme.
 * <p>
 * While the syntax for the rest of the URL may vary depending on the
 * particular scheme selected, URL schemes that involve the direct use
 * of an IP-based protocol to a specified host on the Internet use a
 * common syntax for the scheme-specific data:
 * <pre>
 * //<user>:<password>@<host>:<port>/<url-path>
 * </pre>
 * [RFC2732] specifies that an IPv6 address contained inside a URL
 * must be enclosed in square brackets (to allow distinguishing the
 * colons that separate IPv6 components from the colons that separate
 * scheme-specific data.
 * <p>
 * @author Amit Kapoor
 * @author Hemma Prafullchandra
 * @author Sean Mullan
 * @author Steve Hanna
 * @see GeneralName
 * @see GeneralNames
 * @see GeneralNameInterface
 */
public class URIName implements GeneralNameInterface {

    // private attributes
    private URI uri;
    private String host;
    private DNSName hostDNS;
    private IPAddressName hostIP;

    /**
     * Create the URIName object from the passed encoded Der value.
     *
     * @param derValue the encoded DER URIName.
     * @exception IOException on error.
     */
    public URIName(DerValue derValue) throws IOException {
        this(derValue.getIA5String());
    }

    /**
     * Create the URIName object with the specified name.
     *
     * @param name the URIName.
     * @throws IOException if name is not a proper URIName
     */
    public URIName(String name) throws IOException {
        try {
            uri = new URI(name);
        } catch (URISyntaxException use) {
            throw new IOException("invalid URI name:" + name, use);
        }
        if (uri.getScheme() == null) {
            throw new IOException("URI name must include scheme:" + name);
        }

        host = uri.getHost();
        // RFC 5280 says that the host should be non-null, but we allow it to
        // be null because some widely deployed certificates contain CDP
        // extensions with URIs that have no hostname (see bugs 4802236 and
        // 5107944).
        if (host != null) {
            if (host.charAt(0) == '[') {
                // Verify host is a valid IPv6 address name
                String ipV6Host = host.substring(1, host.length()-1);
                try {
                    hostIP = new IPAddressName(ipV6Host);
                } catch (IOException ioe) {
                    throw new IOException("invalid URI name (host " +
                        "portion is not a valid IPv6 address):" + name);
                }
            } else {
                try {
                    hostDNS = new DNSName(host);
                } catch (IOException ioe) {
                    // Not a valid DNSName; see if it is a valid IPv4
                    // IPAddressName
                    try {
                        hostIP = new IPAddressName(host);
                    } catch (Exception ioe2) {
                        throw new IOException("invalid URI name (host " +
                            "portion is not a valid DNSName, IPv4 address," +
                            " or IPv6 address):" + name);
                    }
                }
            }
        }
    }

    /**
     * Create the URIName object with the specified name constraint. URI
     * name constraints syntax is different than SubjectAltNames, etc. See
     * 4.2.1.10 of RFC 5280.
     *
     * @param value the URI name constraint
     * @throws IOException if name is not a proper URI name constraint
     */
    public static URIName nameConstraint(DerValue value) throws IOException {
        URI uri;
        String name = value.getIA5String();
        try {
            uri = new URI(name);
        } catch (URISyntaxException use) {
            throw new IOException("invalid URI name constraint:" + name, use);
        }
        if (uri.getScheme() == null) {
            String host = uri.getSchemeSpecificPart();
            try {
                DNSName hostDNS;
                if (host.startsWith(".")) {
                    hostDNS = new DNSName(host.substring(1));
                } else {
                    hostDNS = new DNSName(host);
                }
                return new URIName(uri, host, hostDNS);
            } catch (IOException ioe) {
                throw new IOException("invalid URI name constraint:" + name, ioe);
            }
        } else {
            throw new IOException("invalid URI name constraint (should not " +
                "include scheme):" + name);
        }
    }

    URIName(URI uri, String host, DNSName hostDNS) {
        this.uri = uri;
        this.host = host;
        this.hostDNS = hostDNS;
    }

    /**
     * Return the type of the GeneralName.
     */
    public int getType() {
        return GeneralNameInterface.NAME_URI;
    }

    /**
     * Encode the URI name into the DerOutputStream.
     *
     * @param out the DER stream to encode the URIName to.
     * @exception IOException on encoding errors.
     */
    public void encode(DerOutputStream out) throws IOException {
        out.putIA5String(uri.toASCIIString());
    }

    /**
     * Convert the name into user readable string.
     */
    public String toString() {
        return "URIName: " + uri.toString();
    }

    /**
     * Compares this name with another, for equality.
     *
     * @return true iff the names are equivalent according to RFC2459.
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof URIName)) {
            return false;
        }

        URIName other = (URIName) obj;

        return uri.equals(other.getURI());
    }

    /**
     * Returns the URIName as a java.net.URI object
     */
    public URI getURI() {
        return uri;
    }

    /**
     * Returns this URI name.
     */
    public String getName() {
        return uri.toString();
    }

    /**
     * Return the scheme name portion of a URIName
     *
     * @returns scheme portion of full name
     */
    public String getScheme() {
        return uri.getScheme();
    }

    /**
     * Return the host name or IP address portion of the URIName
     *
     * @returns host name or IP address portion of full name
     */
    public String getHost() {
        return host;
    }

    /**
     * Return the host object type; if host name is a
     * DNSName, then this host object does not include any
     * initial "." on the name.
     *
     * @returns host name as DNSName or IPAddressName
     */
    public Object getHostObject() {
        if (hostIP != null) {
            return hostIP;
        } else {
            return hostDNS;
        }
    }

    /**
     * Returns the hash code value for this object.
     *
     * @return a hash code value for this object.
     */
    public int hashCode() {
        return uri.hashCode();
    }

    /**
     * Return type of constraint inputName places on this name:<ul>
     *   <li>NAME_DIFF_TYPE = -1: input name is different type from name
     *       (i.e. does not constrain).
     *   <li>NAME_MATCH = 0: input name matches name.
     *   <li>NAME_NARROWS = 1: input name narrows name (is lower in the naming
     *       subtree)
     *   <li>NAME_WIDENS = 2: input name widens name (is higher in the naming
     *       subtree)
     *   <li>NAME_SAME_TYPE = 3: input name does not match or narrow name, but
     *       is same type.
     * </ul>.
     * These results are used in checking NameConstraints during
     * certification path verification.
     * <p>
     * RFC5280: For URIs, the constraint applies to the host part of the name.
     * The constraint may specify a host or a domain.  Examples would be
     * "foo.bar.com";  and ".xyz.com".  When the the constraint begins with
     * a period, it may be expanded with one or more subdomains.  That is,
     * the constraint ".xyz.com" is satisfied by both abc.xyz.com and
     * abc.def.xyz.com.  However, the constraint ".xyz.com" is not satisfied
     * by "xyz.com".  When the constraint does not begin with a period, it
     * specifies a host.
     * <p>
     * @param inputName to be checked for being constrained
     * @returns constraint type above
     * @throws UnsupportedOperationException if name is not exact match, but
     *  narrowing and widening are not supported for this name type.
     */
    public int constrains(GeneralNameInterface inputName)
        throws UnsupportedOperationException {
        int constraintType;
        if (inputName == null) {
            constraintType = NAME_DIFF_TYPE;
        } else if (inputName.getType() != NAME_URI) {
            constraintType = NAME_DIFF_TYPE;
        } else {
            // Assuming from here on that one or both of these is
            // actually a URI name constraint (not a URI), so we
            // only need to compare the host portion of the name

            String otherHost = ((URIName)inputName).getHost();

            // Quick check for equality
            if (otherHost.equalsIgnoreCase(host)) {
                constraintType = NAME_MATCH;
            } else {
                Object otherHostObject = ((URIName)inputName).getHostObject();

                if ((hostDNS == null) ||
                    !(otherHostObject instanceof DNSName)) {
                    // If one (or both) is an IP address, only same type
                    constraintType = NAME_SAME_TYPE;
                } else {
                    // Both host portions are DNSNames. Are they domains?
                    boolean thisDomain = (host.charAt(0) == '.');
                    boolean otherDomain = (otherHost.charAt(0) == '.');
                    DNSName otherDNS = (DNSName) otherHostObject;

                    // Run DNSName.constrains.
                    constraintType = hostDNS.constrains(otherDNS);
                    // If neither one is a domain, then they can't
                    // widen or narrow. That's just SAME_TYPE.
                    if ((!thisDomain && !otherDomain) &&
                        ((constraintType == NAME_WIDENS) ||
                         (constraintType == NAME_NARROWS))) {
                        constraintType = NAME_SAME_TYPE;
                    }

                    // If one is a domain and the other isn't,
                    // then they can't match. The one that's a
                    // domain doesn't include the one that's
                    // not a domain.
                    if ((thisDomain != otherDomain) &&
                        (constraintType == NAME_MATCH)) {
                        if (thisDomain) {
                            constraintType = NAME_WIDENS;
                        } else {
                            constraintType = NAME_NARROWS;
                        }
                    }
                }
            }
        }
        return constraintType;
    }

    /**
     * Return subtree depth of this name for purposes of determining
     * NameConstraints minimum and maximum bounds and for calculating
     * path lengths in name subtrees.
     *
     * @returns distance of name from root
     * @throws UnsupportedOperationException if not supported for this name type
     */
    public int subtreeDepth() throws UnsupportedOperationException {
        DNSName dnsName = null;
        try {
            dnsName = new DNSName(host);
        } catch (IOException ioe) {
            throw new UnsupportedOperationException(ioe.getMessage());
        }
        return dnsName.subtreeDepth();
    }
}
