/*
 * Copyright (c) 2000, 2011, 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.
 */

/*
 *
 *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
 *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
 */

package sun.security.krb5;

import sun.security.krb5.internal.*;
import sun.security.util.*;
import java.net.*;
import java.util.Vector;
import java.util.Locale;
import java.io.IOException;
import java.math.BigInteger;
import sun.security.krb5.internal.ccache.CCacheOutputStream;
import sun.security.krb5.internal.util.KerberosString;


/**
 * This class encapsulates a Kerberos principal.
 */
public class PrincipalName
    implements Cloneable {

    //name types

    /**
     * Name type not known
     */
    public static final int KRB_NT_UNKNOWN =   0;

    /**
     * Just the name of the principal as in DCE, or for users
     */
    public static final int KRB_NT_PRINCIPAL = 1;

    /**
     * Service and other unique instance (krbtgt)
     */
    public static final int KRB_NT_SRV_INST =  2;

    /**
     * Service with host name as instance (telnet, rcommands)
     */
    public static final int KRB_NT_SRV_HST =   3;

    /**
     * Service with host as remaining components
     */
    public static final int KRB_NT_SRV_XHST =  4;

    /**
     * Unique ID
     */
    public static final int KRB_NT_UID = 5;



    /**
     * TGS Name
     */
    public static final String TGS_DEFAULT_SRV_NAME = "krbtgt";
    public static final int TGS_DEFAULT_NT = KRB_NT_SRV_INST;

    public static final char NAME_COMPONENT_SEPARATOR = '/';
    public static final char NAME_REALM_SEPARATOR = '@';
    public static final char REALM_COMPONENT_SEPARATOR = '.';

    public static final String NAME_COMPONENT_SEPARATOR_STR = "/";
    public static final String NAME_REALM_SEPARATOR_STR = "@";
    public static final String REALM_COMPONENT_SEPARATOR_STR = ".";

    private int nameType;
    private String[] nameStrings;  // Principal names don't mutate often

    private Realm nameRealm;  // optional; a null realm means use default
    // Note: the nameRealm is not included in the default ASN.1 encoding

    // cached salt, might be changed by KDC info, not used in clone
    private String salt = null;

    protected PrincipalName() {
    }

    public PrincipalName(String[] nameParts, int type)
        throws IllegalArgumentException, IOException {
        if (nameParts == null) {
            throw new IllegalArgumentException("Null input not allowed");
        }
        nameStrings = new String[nameParts.length];
        System.arraycopy(nameParts, 0, nameStrings, 0, nameParts.length);
        nameType = type;
        nameRealm = null;
    }

    public PrincipalName(String[] nameParts) throws IOException {
        this(nameParts, KRB_NT_UNKNOWN);
    }

    public Object clone() {
        try {
            PrincipalName pName = (PrincipalName) super.clone();
            // Re-assign mutable fields
            if (nameStrings != null) {
                pName.nameStrings = nameStrings.clone();
            }
            if (nameRealm != null) {
                pName.nameRealm = (Realm)nameRealm.clone();
            }
            return pName;
        } catch (CloneNotSupportedException ex) {
            throw new AssertionError("Should never happen");
        }
    }

    /*
     * Added to workaround a bug where the equals method that takes a
     * PrincipalName is not being called but Object.equals(Object) is
     * being called.
     */
    public boolean equals(Object o) {
        if (o instanceof PrincipalName)
            return equals((PrincipalName)o);
        else
            return false;
    }

    public boolean equals(PrincipalName other) {


        if (!equalsWithoutRealm(other)) {
            return false;
        }

        if ((nameRealm != null && other.nameRealm == null) ||
            (nameRealm == null && other.nameRealm != null)) {
            return false;
        }

        if (nameRealm != null && other.nameRealm != null) {
            if (!nameRealm.equals(other.nameRealm)) {
                return false;
            }
        }

        return true;
    }

    boolean equalsWithoutRealm(PrincipalName other) {

        if ((nameStrings != null && other.nameStrings == null) ||
            (nameStrings == null && other.nameStrings != null))
            return false;

        if (nameStrings != null && other.nameStrings != null) {
            if (nameStrings.length != other.nameStrings.length)
                return false;
            for (int i = 0; i < nameStrings.length; i++)
                if (!nameStrings[i].equals(other.nameStrings[i]))
                    return false;
        }

        return true;

    }

    /**
     * Returns the ASN.1 encoding of the
     * <xmp>
     * PrincipalName    ::= SEQUENCE {
     *          name-type       [0] Int32,
     *          name-string     [1] SEQUENCE OF KerberosString
     * }
     *
     * KerberosString   ::= GeneralString (IA5String)
     * </xmp>
     *
     * <p>
     * This definition reflects the Network Working Group RFC 4120
     * specification available at
     * <a href="http://www.ietf.org/rfc/rfc4120.txt">
     * http://www.ietf.org/rfc/rfc4120.txt</a>.
     *
     * @param encoding a Der-encoded data.
     * @exception Asn1Exception if an error occurs while decoding
     * an ASN1 encoded data.
     * @exception Asn1Exception if there is an ASN1 encoding error
     * @exception IOException if an I/O error occurs
     * @exception IllegalArgumentException if encoding is null
     * reading encoded data.
     *
     */
    public PrincipalName(DerValue encoding)
        throws Asn1Exception, IOException {
        nameRealm = null;
        DerValue der;
        if (encoding == null) {
            throw new IllegalArgumentException("Null input not allowed");
        }
        if (encoding.getTag() != DerValue.tag_Sequence) {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
        der = encoding.getData().getDerValue();
        if ((der.getTag() & 0x1F) == 0x00) {
            BigInteger bint = der.getData().getBigInteger();
            nameType = bint.intValue();
        } else {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
        der = encoding.getData().getDerValue();
        if ((der.getTag() & 0x01F) == 0x01) {
            DerValue subDer = der.getData().getDerValue();
            if (subDer.getTag() != DerValue.tag_SequenceOf) {
                throw new Asn1Exception(Krb5.ASN1_BAD_ID);
            }
            Vector<String> v = new Vector<>();
            DerValue subSubDer;
            while(subDer.getData().available() > 0) {
                subSubDer = subDer.getData().getDerValue();
                v.addElement(new KerberosString(subSubDer).toString());
            }
            if (v.size() > 0) {
                nameStrings = new String[v.size()];
                v.copyInto(nameStrings);
            } else {
                nameStrings = new String[] {""};
            }
        } else  {
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        }
    }

    /**
     * Parse (unmarshal) a <code>PrincipalName</code> from a DER
     * input stream.  This form
     * parsing might be used when expanding a value which is part of
     * a constructed sequence and uses explicitly tagged type.
     *
     * @exception Asn1Exception on error.
     * @param data the Der input stream value, which contains one or
     * more marshaled value.
     * @param explicitTag tag number.
     * @param optional indicate if this data field is optional
     * @return an instance of <code>PrincipalName</code>.
     *
     */
    public static PrincipalName parse(DerInputStream data,
                                      byte explicitTag, boolean
                                      optional)
        throws Asn1Exception, IOException {

        if ((optional) && (((byte)data.peekByte() & (byte)0x1F) !=
                           explicitTag))
            return null;
        DerValue der = data.getDerValue();
        if (explicitTag != (der.getTag() & (byte)0x1F))
            throw new Asn1Exception(Krb5.ASN1_BAD_ID);
        else {
            DerValue subDer = der.getData().getDerValue();
            return new PrincipalName(subDer);
        }
    }


    // This is protected because the definition of a principal
    // string is fixed
    // XXX Error checkin consistent with MIT krb5_parse_name
    // Code repetition, realm parsed again by class Realm
    protected static String[] parseName(String name) {

        Vector<String> tempStrings = new Vector<>();
        String temp = name;
        int i = 0;
        int componentStart = 0;
        String component;

        while (i < temp.length()) {
            if (temp.charAt(i) == NAME_COMPONENT_SEPARATOR) {
                /*
                 * If this separator is escaped then don't treat it
                 * as a separator
                 */
                if (i > 0 && temp.charAt(i - 1) == '\\') {
                    temp = temp.substring(0, i - 1) +
                        temp.substring(i, temp.length());
                    continue;
                }
                else {
                    if (componentStart < i) {
                        component = temp.substring(componentStart, i);
                        tempStrings.addElement(component);
                    }
                    componentStart = i + 1;
                }
            } else
                if (temp.charAt(i) == NAME_REALM_SEPARATOR) {
                    /*
                     * If this separator is escaped then don't treat it
                     * as a separator
                     */
                    if (i > 0 && temp.charAt(i - 1) == '\\') {
                        temp = temp.substring(0, i - 1) +
                            temp.substring(i, temp.length());
                        continue;
                    } else {
                        if (componentStart < i) {
                            component = temp.substring(componentStart, i);
                            tempStrings.addElement(component);
                        }
                        componentStart = i + 1;
                        break;
                    }
                }
            i++;
        }

        if (i == temp.length())
        if (componentStart < i) {
            component = temp.substring(componentStart, i);
            tempStrings.addElement(component);
        }

        String[] result = new String[tempStrings.size()];
        tempStrings.copyInto(result);
        return result;
    }

    public PrincipalName(String name, int type)
        throws RealmException {
        if (name == null) {
            throw new IllegalArgumentException("Null name not allowed");
        }
        String[] nameParts = parseName(name);
        Realm tempRealm = null;
        String realmString = Realm.parseRealmAtSeparator(name);

        if (realmString == null) {
            try {
                Config config = Config.getInstance();
                realmString = config.getDefaultRealm();
            } catch (KrbException e) {
                RealmException re =
                    new RealmException(e.getMessage());
                re.initCause(e);
                throw re;
            }
        }

        if (realmString != null)
            tempRealm = new Realm(realmString);

        switch (type) {
        case KRB_NT_SRV_HST:
            if (nameParts.length >= 2) {
                String hostName = nameParts[1];
                try {
                    // RFC4120 does not recommend canonicalizing a hostname.
                    // However, for compatibility reason, we will try
                    // canonicalize it and see if the output looks better.

                    String canonicalized = (InetAddress.getByName(hostName)).
                            getCanonicalHostName();

                    // Looks if canonicalized is a longer format of hostName,
                    // we accept cases like
                    //     bunny -> bunny.rabbit.hole
                    if (canonicalized.toLowerCase(Locale.ENGLISH).startsWith(
                                hostName.toLowerCase(Locale.ENGLISH)+".")) {
                        hostName = canonicalized;
                    }
                } catch (UnknownHostException e) {
                    // no canonicalization, use old
                }
                nameParts[1] = hostName.toLowerCase(Locale.ENGLISH);
            }
            nameStrings = nameParts;
            nameType = type;
                // We will try to get realm name from the mapping in
                // the configuration. If it is not specified
                // we will use the default realm. This nametype does
                // not allow a realm to be specified. The name string must of
                // the form service@host and this is internally changed into
                // service/host by Kerberos

            String mapRealm =  mapHostToRealm(nameParts[1]);
            if (mapRealm != null) {
                nameRealm = new Realm(mapRealm);
            } else {
                nameRealm = tempRealm;
            }
            break;
        case KRB_NT_UNKNOWN:
        case KRB_NT_PRINCIPAL:
        case KRB_NT_SRV_INST:
        case KRB_NT_SRV_XHST:
        case KRB_NT_UID:
            nameStrings = nameParts;
            nameType = type;
            nameRealm = tempRealm;
            break;
        default:
            throw new IllegalArgumentException("Illegal name type");
        }
    }

    public PrincipalName(String name) throws RealmException {
        this(name, KRB_NT_UNKNOWN);
    }

    public PrincipalName(String name, String realm) throws RealmException {
        this(name, KRB_NT_UNKNOWN);
        nameRealm = new Realm(realm);
    }

    public String getRealmAsString() {
        return getRealmString();
    }

    public String getPrincipalNameAsString() {
        StringBuffer temp = new StringBuffer(nameStrings[0]);
        for (int i = 1; i < nameStrings.length; i++)
            temp.append(nameStrings[i]);
        return temp.toString();
    }

    public int hashCode() {
        return toString().hashCode();
    }

    public String getName() {
        return toString();
    }

    public int getNameType() {
        return nameType;
    }

    public String[] getNameStrings() {
        return nameStrings;
    }

    public byte[][] toByteArray() {
        byte[][] result = new byte[nameStrings.length][];
        for (int i = 0; i < nameStrings.length; i++) {
            result[i] = new byte[nameStrings[i].length()];
            result[i] = nameStrings[i].getBytes();
        }
        return result;
    }

    public String getRealmString() {
        if (nameRealm != null)
            return nameRealm.toString();
        return null;
    }

    public Realm getRealm() {
        return nameRealm;
    }

    public void setRealm(Realm new_nameRealm) throws RealmException {
        nameRealm = new_nameRealm;
    }

    public void setRealm(String realmsString) throws RealmException {
        nameRealm = new Realm(realmsString);
    }

    public String getSalt() {
        if (salt == null) {
            StringBuffer salt = new StringBuffer();
            if (nameRealm != null) {
                salt.append(nameRealm.toString());
            }
            for (int i = 0; i < nameStrings.length; i++) {
                salt.append(nameStrings[i]);
            }
            return salt.toString();
        }
        return salt;
    }

    public String toString() {
        StringBuffer str = new StringBuffer();
        for (int i = 0; i < nameStrings.length; i++) {
            if (i > 0)
                str.append("/");
            str.append(nameStrings[i]);
        }
        if (nameRealm != null) {
            str.append("@");
            str.append(nameRealm.toString());
        }

        return str.toString();
    }

    public String getNameString() {
        StringBuffer str = new StringBuffer();
        for (int i = 0; i < nameStrings.length; i++) {
            if (i > 0)
                str.append("/");
            str.append(nameStrings[i]);
        }
        return str.toString();
    }

    /**
     * Encodes a <code>PrincipalName</code> object.
     * @return the byte array of the encoded PrncipalName object.
     * @exception Asn1Exception if an error occurs while decoding an ASN1 encoded data.
     * @exception IOException if an I/O error occurs while reading encoded data.
     *
     */
    public byte[] asn1Encode() throws Asn1Exception, IOException {
        DerOutputStream bytes = new DerOutputStream();
        DerOutputStream temp = new DerOutputStream();
        BigInteger bint = BigInteger.valueOf(this.nameType);
        temp.putInteger(bint);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0x00), temp);
        temp = new DerOutputStream();
        DerValue der[] = new DerValue[nameStrings.length];
        for (int i = 0; i < nameStrings.length; i++) {
            der[i] = new KerberosString(nameStrings[i]).toDerValue();
        }
        temp.putSequence(der);
        bytes.write(DerValue.createTag(DerValue.TAG_CONTEXT, true, (byte)0x01), temp);
        temp = new DerOutputStream();
        temp.write(DerValue.tag_Sequence, bytes);
        return temp.toByteArray();
    }


    /**
     * Checks if two <code>PrincipalName</code> objects have identical values in their corresponding data fields.
     *
     * @param pname the other <code>PrincipalName</code> object.
     * @return true if two have identical values, otherwise, return false.
     */
    // It is used in <code>sun.security.krb5.internal.ccache</code> package.
    public boolean match(PrincipalName pname) {
        boolean matched = true;
        //name type is just a hint, no two names can be the same ignoring name type.
        // if (this.nameType != pname.nameType) {
        //      matched = false;
        // }
        if ((this.nameRealm != null) && (pname.nameRealm != null)) {
            if (!(this.nameRealm.toString().equalsIgnoreCase(pname.nameRealm.toString()))) {
                matched = false;
            }
        }
        if (this.nameStrings.length != pname.nameStrings.length) {
            matched = false;
        } else {
            for (int i = 0; i < this.nameStrings.length; i++) {
                if (!(this.nameStrings[i].equalsIgnoreCase(pname.nameStrings[i]))) {
                    matched = false;
                }
            }
        }
        return matched;
    }

    /**
     * Writes data field values of <code>PrincipalName</code> in FCC format to an output stream.
     *
     * @param cos a <code>CCacheOutputStream</code> for writing data.
     * @exception IOException if an I/O exception occurs.
     * @see sun.security.krb5.internal.ccache.CCacheOutputStream
     */
    public void writePrincipal(CCacheOutputStream cos) throws IOException {
        cos.write32(nameType);
        cos.write32(nameStrings.length);
        if (nameRealm != null) {
            byte[] realmBytes = null;
            realmBytes = nameRealm.toString().getBytes();
            cos.write32(realmBytes.length);
            cos.write(realmBytes, 0, realmBytes.length);
        }
        byte[] bytes = null;
        for (int i = 0; i < nameStrings.length; i++) {
            bytes = nameStrings[i].getBytes();
            cos.write32(bytes.length);
            cos.write(bytes, 0, bytes.length);
        }
    }

    /**
     * Creates a KRB_NT_SRV_INST name from the supplied
     * name components and realm.
     * @param primary the primary component of the name
     * @param instance the instance component of the name
     * @param realm the realm
     * @throws KrbException
     */
    protected PrincipalName(String primary, String instance, String realm,
                            int type)
        throws KrbException {

        if (type != KRB_NT_SRV_INST) {
            throw new KrbException(Krb5.KRB_ERR_GENERIC, "Bad name type");
        }

        String[] nParts = new String[2];
        nParts[0] = primary;
        nParts[1] = instance;

        this.nameStrings = nParts;
        this.nameRealm = new Realm(realm);
        this.nameType = type;
    }

    /**
     * Returns the instance component of a name.
     * In a multi-component name such as a KRB_NT_SRV_INST
     * name, the second component is returned.
     * Null is returned if there are not two or more
     * components in the name.
     * @returns instance component of a multi-component name.
     */
    public String getInstanceComponent()
    {
        if (nameStrings != null && nameStrings.length >= 2)
            {
                return new String(nameStrings[1]);
            }

        return null;
    }

    static String mapHostToRealm(String name) {
        String result = null;
        try {
            String subname = null;
            Config c = Config.getInstance();
            if ((result = c.getDefault(name, "domain_realm")) != null)
                return result;
            else {
                for (int i = 1; i < name.length(); i++) {
                    if ((name.charAt(i) == '.') && (i != name.length() - 1)) { //mapping could be .ibm.com = AUSTIN.IBM.COM
                        subname = name.substring(i);
                        result = c.getDefault(subname, "domain_realm");
                        if (result != null) {
                            break;
                        }
                        else {
                            subname = name.substring(i + 1);      //or mapping could be ibm.com = AUSTIN.IBM.COM
                            result = c.getDefault(subname, "domain_realm");
                            if (result != null) {
                                break;
                            }
                        }
                    }
                }
            }
        } catch (KrbException e) {
        }
        return result;
    }

}
