/*
 * Copyright (c) 1999, 2024, 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 com.sun.jndi.ldap;

import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.spi.DirectoryManager;
import javax.naming.spi.DirStateFactory;

import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.InputStream;

import java.util.ArrayList;
import java.util.Base64;
import java.util.Hashtable;
import java.util.StringTokenizer;

import java.lang.reflect.Proxy;
import java.lang.reflect.Modifier;

/**
  * Class containing static methods and constants for dealing with
  * encoding/decoding JNDI References and Serialized Objects
  * in LDAP.
  * @author Vincent Ryan
  * @author Rosanna Lee
  */
final class Obj {

    private Obj () {}; // Make sure no one can create one

    /**
     * Determines whether objects may be deserialized or reconstructed from a content of
     * 'javaSerializedData', 'javaRemoteLocation' or 'javaReferenceAddress' LDAP attributes.
     */
    private static final boolean trustSerialData;

    static {
        // System property to control whether classes are allowed to be loaded from
        // 'javaSerializedData', 'javaRemoteLocation' or 'javaReferenceAddress' attributes.
        String trustSerialDataSp = System.getProperty(
                "com.sun.jndi.ldap.object.trustSerialData", "false");
        trustSerialData = "true".equalsIgnoreCase(trustSerialDataSp);
    }

    // LDAP attributes used to support Java objects.
    static final String[] JAVA_ATTRIBUTES = {
        "objectClass",
        "javaSerializedData",
        "javaClassName",
        "javaFactory",
        "javaCodeBase",
        "javaReferenceAddress",
        "javaClassNames",
        "javaRemoteLocation"     // Deprecated
    };

    static final int OBJECT_CLASS = 0;
    static final int SERIALIZED_DATA = 1;
    static final int CLASSNAME = 2;
    static final int FACTORY = 3;
    static final int CODEBASE = 4;
    static final int REF_ADDR = 5;
    static final int TYPENAME = 6;
    /**
     * @deprecated
     */
    @Deprecated
    private static final int REMOTE_LOC = 7;

    // LDAP object classes to support Java objects
    static final String[] JAVA_OBJECT_CLASSES = {
        "javaContainer",
        "javaObject",
        "javaNamingReference",
        "javaSerializedObject",
        "javaMarshalledObject",
    };

    static final String[] JAVA_OBJECT_CLASSES_LOWER = {
        "javacontainer",
        "javaobject",
        "javanamingreference",
        "javaserializedobject",
        "javamarshalledobject",
    };

    static final int STRUCTURAL = 0;    // structural object class
    static final int BASE_OBJECT = 1;   // auxiliary java object class
    static final int REF_OBJECT = 2;    // auxiliary reference object class
    static final int SER_OBJECT = 3;    // auxiliary serialized object class
    static final int MAR_OBJECT = 4;    // auxiliary marshalled object class

    /**
     * Encode an object in LDAP attributes.
     * Supports binding Referenceable or Reference, Serializable,
     * and DirContext.
     *
     * If the object supports the Referenceable interface then encode
     * the reference to the object. See encodeReference() for details.
     *<p>
     * If the object is serializable, it is stored as follows:
     * javaClassName
     *   value: Object.getClass();
     * javaSerializedData
     *   value: serialized form of Object (in binary form).
     * javaTypeName
     *   value: getTypeNames(Object.getClass());
     */
    private static Attributes encodeObject(char separator,
        Object obj, Attributes attrs,
        Attribute objectClass, boolean cloned)
        throws NamingException {
            boolean structural =
                (objectClass.size() == 0 ||
                    (objectClass.size() == 1 && objectClass.contains("top")));

            if (structural) {
                objectClass.add(JAVA_OBJECT_CLASSES[STRUCTURAL]);
            }

    // References
            if (obj instanceof Referenceable) {
                objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
                objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]);
                if (!cloned) {
                    attrs = (Attributes)attrs.clone();
                }
                attrs.put(objectClass);
                return (encodeReference(separator,
                    ((Referenceable)obj).getReference(),
                    attrs, obj));

            } else if (obj instanceof Reference) {
                objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
                objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]);
                if (!cloned) {
                    attrs = (Attributes)attrs.clone();
                }
                attrs.put(objectClass);
                return (encodeReference(separator, (Reference)obj, attrs, null));

    // Serializable Object
            } else if (obj instanceof java.io.Serializable) {
                objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
                if (!(objectClass.contains(JAVA_OBJECT_CLASSES[MAR_OBJECT]) ||
                    objectClass.contains(JAVA_OBJECT_CLASSES_LOWER[MAR_OBJECT]))) {
                    objectClass.add(JAVA_OBJECT_CLASSES[SER_OBJECT]);
                }
                if (!cloned) {
                    attrs = (Attributes)attrs.clone();
                }
                attrs.put(objectClass);
                attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[SERIALIZED_DATA],
                    serializeObject(obj)));
                if (attrs.get(JAVA_ATTRIBUTES[CLASSNAME]) == null) {
                    attrs.put(JAVA_ATTRIBUTES[CLASSNAME],
                        obj.getClass().getName());
                }
                if (attrs.get(JAVA_ATTRIBUTES[TYPENAME]) == null) {
                    Attribute tAttr =
                        LdapCtxFactory.createTypeNameAttr(obj.getClass());
                    if (tAttr != null) {
                        attrs.put(tAttr);
                    }
                }
    // DirContext Object
            } else if (obj instanceof DirContext) {
                // do nothing
            } else {
                throw new IllegalArgumentException(
            "can only bind Referenceable, Serializable, DirContext");
            }
            //      System.err.println(attrs);
            return attrs;
    }

    /**
     * Each value in javaCodebase contains a list of space-separated
     * URLs. Each value is independent; we can pick any of the values
     * so we just use the first one.
     * @return an array of URL strings for the codebase
     */
    private static String[] getCodebases(Attribute codebaseAttr) throws
        NamingException {
        if (codebaseAttr == null) {
            return null;
        } else {
            StringTokenizer parser =
                new StringTokenizer((String)codebaseAttr.get());
            ArrayList<String> list = new ArrayList<>(10);
            while (parser.hasMoreTokens()) {
                list.add(parser.nextToken());
            }
            String[] answer = new String[list.size()];
            for (int i = 0; i < answer.length; i++) {
                answer[i] = list.get(i);
            }
            return answer;
        }
    }

    /*
     * Decode an object from LDAP attribute(s).
     * The object may be a Reference, or a Serialized object.
     *
     * See encodeObject() and encodeReference() for details on formats
     * expected.
     */
    static Object decodeObject(Attributes attrs)
        throws NamingException {

        Attribute attr;

        // Get codebase, which is used in all 3 cases.
        String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
        try {
            if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
                if (!trustSerialData) {
                    throw new NamingException("Object deserialization is not allowed");
                }
                ClassLoader cl = Thread.currentThread().getContextClassLoader();
                return deserializeObject((byte[])attr.get(), cl);
            } else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
                 // javaRemoteLocation attribute (RMI stub will be created)
                 if (!trustSerialData) {
                     throw new NamingException("Object deserialization is not allowed");
                 }
                // For backward compatibility only
                return decodeRmiObject(
                    (String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
                    (String)attr.get());
            }

            attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
            if (attr != null &&
                (attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
                    attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
                return decodeReference(attrs, codebases);
            }
            return null;
        } catch (IOException e) {
            NamingException ne = new NamingException();
            ne.setRootCause(e);
            throw ne;
        }
    }

    /**
     * Convert a Reference object into several LDAP attributes.
     *
     * A Reference is stored as into the following attributes:
     * javaClassName
     *   value: Reference.getClassName();
     * javaFactory
     *   value: Reference.getFactoryClassName();
     * javaCodeBase
     *   value: Reference.getFactoryClassLocation();
     * javaReferenceAddress
     *   value: #0#typeA#valA
     *   value: #1#typeB#valB
     *   value: #2#typeC##[serialized RefAddr C]
     *   value: #3#typeD#valD
     *
     * where
     * -  the first character denotes the separator
     * -  the number following the first separator denotes the position
     *    of the RefAddr within the Reference
     * -  "typeA" is RefAddr.getType()
     * -  ## denotes that the Base64-encoded form of the non-StringRefAddr
     *    is to follow; otherwise the value that follows is
     *    StringRefAddr.getContents()
     *
     * The default separator is the hash character (#).
     * May provide property for this in future.
     */

    private static Attributes encodeReference(char separator,
        Reference ref, Attributes attrs, Object orig)
        throws NamingException {

        if (ref == null)
            return attrs;

        String s;

        if ((s = ref.getClassName()) != null) {
            attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[CLASSNAME], s));
        }

        if ((s = ref.getFactoryClassName()) != null) {
            attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[FACTORY], s));
        }

        if ((s = ref.getFactoryClassLocation()) != null) {
            attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[CODEBASE], s));
        }

        // Get original object's types if caller has not explicitly
        // specified other type names
        if (orig != null && attrs.get(JAVA_ATTRIBUTES[TYPENAME]) != null) {
            Attribute tAttr =
                LdapCtxFactory.createTypeNameAttr(orig.getClass());
            if (tAttr != null) {
                attrs.put(tAttr);
            }
        }

        int count = ref.size();

        if (count > 0) {

            Attribute refAttr = new BasicAttribute(JAVA_ATTRIBUTES[REF_ADDR]);
            RefAddr refAddr;
            Base64.Encoder encoder = null;

            for (int i = 0; i < count; i++) {
                refAddr = ref.get(i);

                if (refAddr instanceof StringRefAddr) {
                    refAttr.add(""+ separator + i +
                        separator +     refAddr.getType() +
                        separator + refAddr.getContent());
                } else {
                    if (encoder == null)
                        encoder = Base64.getMimeEncoder();

                    refAttr.add(""+ separator + i +
                        separator + refAddr.getType() +
                        separator + separator +
                        encoder.encodeToString(serializeObject(refAddr)));
                }
            }
            attrs.put(refAttr);
        }
        return attrs;
    }

    /*
     * A RMI object is stored in the directory as
     * javaClassName
     *   value: Object.getClass();
     * javaRemoteLocation
     *   value: URL of RMI object (accessed through the RMI Registry)
     * javaCodebase:
     *   value: URL of codebase of where to find classes for object
     *
     * Return the RMI Location URL itself. This will be turned into
     * an RMI object when getObjectInstance() is called on it.
     * %%% Ignore codebase for now. Depend on RMI registry to send code.-RL
     * @deprecated For backward compatibility only
     */
    private static Object decodeRmiObject(String className,
                                          String rmiName) throws NamingException {
            return new Reference(className, new StringRefAddr("URL", rmiName));
    }

    /*
     * Restore a Reference object from several LDAP attributes
     */
    private static Reference decodeReference(Attributes attrs,
        String[] codebases) throws NamingException, IOException {

        Attribute attr;
        String className;
        String factory = null;

        if ((attr = attrs.get(JAVA_ATTRIBUTES[CLASSNAME])) != null) {
            className = (String)attr.get();
        } else {
            throw new InvalidAttributesException(JAVA_ATTRIBUTES[CLASSNAME] +
                        " attribute is required");
        }

        if ((attr = attrs.get(JAVA_ATTRIBUTES[FACTORY])) != null) {
            factory = (String)attr.get();
        }

        Reference ref = new Reference(className, factory,
            (codebases != null? codebases[0] : null));

        /*
         * string encoding of a RefAddr is either:
         *
         *      #posn#<type>#<address>
         * or
         *      #posn#<type>##<base64-encoded address>
         */
        if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null) {

            String val, posnStr, type;
            char separator;
            int start, sep, posn;
            Base64.Decoder decoder = null;

            ClassLoader cl = Thread.currentThread().getContextClassLoader();

            /*
             * Temporary array for decoded RefAddr addresses - used to ensure
             * unordered addresses are correctly re-ordered.
             */
            RefAddr[] refAddrList = new RefAddr[attr.size()];

            for (NamingEnumeration<?> vals = attr.getAll(); vals.hasMore(); ) {

                val = (String)vals.next();

                if (val.length() == 0) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - "+
                        "empty attribute value");
                }
                // first character denotes encoding separator
                separator = val.charAt(0);
                start = 1;  // skip over separator

                // extract position within Reference
                if ((sep = val.indexOf(separator, start)) < 0) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " +
                        "separator '" + separator + "'" + "not found");
                }
                if ((posnStr = val.substring(start, sep)) == null) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " +
                        "empty RefAddr position");
                }
                try {
                    posn = Integer.parseInt(posnStr);
                } catch (NumberFormatException nfe) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " +
                        "RefAddr position not an integer");
                }
                start = sep + 1; // skip over position and trailing separator

                // extract type
                if ((sep = val.indexOf(separator, start)) < 0) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " +
                        "RefAddr type not found");
                }
                if ((type = val.substring(start, sep)) == null) {
                    throw new InvalidAttributeValueException(
                        "malformed " + JAVA_ATTRIBUTES[REF_ADDR] + " attribute - " +
                        "empty RefAddr type");
                }
                start = sep + 1; // skip over type and trailing separator

                // extract content
                if (start == val.length()) {
                    // Empty content
                    refAddrList[posn] = new StringRefAddr(type, null);
                } else if (val.charAt(start) == separator) {
                    // Check if deserialization of binary RefAddr is allowed from
                    // 'javaReferenceAddress' LDAP attribute.
                    if (!trustSerialData) {
                        throw new NamingException("Object deserialization is not allowed");
                    }

                    // Double separators indicate a non-StringRefAddr
                    // Content is a Base64-encoded serialized RefAddr

                    ++start;  // skip over consecutive separator
                    // %%% RL: exception if empty after double separator

                    if (decoder == null)
                        decoder = Base64.getMimeDecoder();

                    RefAddr ra = (RefAddr)
                        deserializeObject(
                            decoder.decode(val.substring(start).getBytes()),
                            cl);

                    refAddrList[posn] = ra;
                } else {
                    // Single separator indicates a StringRefAddr
                    refAddrList[posn] = new StringRefAddr(type,
                        val.substring(start));
                }
            }

            // Copy to real reference
            for (int i = 0; i < refAddrList.length; i++) {
                ref.add(refAddrList[i]);
            }
        }

        return (ref);
    }

    /*
     * Serialize an object into a byte array
     */
    private static byte[] serializeObject(Object obj) throws NamingException {

        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (ObjectOutputStream serial = new ObjectOutputStream(bytes)) {
                serial.writeObject(obj);
            }

            return (bytes.toByteArray());

        } catch (IOException e) {
            NamingException ne = new NamingException();
            ne.setRootCause(e);
            throw ne;
        }
    }

    /*
     * Deserializes a byte array into an object.
     */
    private static Object deserializeObject(byte[] obj, ClassLoader cl)
        throws NamingException {

        try {
            // Create ObjectInputStream for deserialization
            ByteArrayInputStream bytes = new ByteArrayInputStream(obj);
            try (ObjectInputStream deserial = cl == null ?
                    new ObjectInputStream(bytes) :
                    new LoaderInputStream(bytes, cl)) {
                return deserial.readObject();
            } catch (ClassNotFoundException e) {
                NamingException ne = new NamingException();
                ne.setRootCause(e);
                throw ne;
            }
        } catch (IOException e) {
            NamingException ne = new NamingException();
            ne.setRootCause(e);
            throw ne;
        }
    }

    /**
      * Returns the attributes to bind given an object and its attributes.
      */
    static Attributes determineBindAttrs(
        char separator, Object obj, Attributes attrs, boolean cloned,
        Name name, Context ctx, Hashtable<?,?> env)
        throws NamingException {

        // Call state factories to convert object and attrs
        DirStateFactory.Result res =
            DirectoryManager.getStateToBind(obj, name, ctx, env, attrs);
        obj = res.getObject();
        attrs = res.getAttributes();

        // We're only storing attributes; no further processing required
        if (obj == null) {
            return attrs;
        }

        //if object to be bound is a DirContext extract its attributes
        if ((attrs == null) && (obj instanceof DirContext)) {
            cloned = true;
            attrs = ((DirContext)obj).getAttributes("");
        }

        boolean ocNeedsCloning = false;

        // Create "objectClass" attribute
        Attribute objectClass;
        if (attrs == null || attrs.size() == 0) {
            attrs = new BasicAttributes(LdapClient.caseIgnore);
            cloned = true;

            // No objectclasses supplied, use "top" to start
            objectClass = new BasicAttribute("objectClass", "top");

        } else {
            // Get existing objectclass attribute
            objectClass = attrs.get("objectClass");
            if (objectClass == null && !attrs.isCaseIgnored()) {
                // %%% workaround
                objectClass = attrs.get("objectclass");
            }

            // No objectclasses supplied, use "top" to start
            if (objectClass == null) {
                objectClass =  new BasicAttribute("objectClass", "top");
            } else if (ocNeedsCloning || !cloned) {
                objectClass = (Attribute)objectClass.clone();
            }
        }

        // convert the supplied object into LDAP attributes
        attrs = encodeObject(separator, obj, attrs, objectClass, cloned);

        // System.err.println("Determined: " + attrs);
        return attrs;
    }

    /**
     * An ObjectInputStream that uses a class loader to find classes.
     */
    private static final class LoaderInputStream extends ObjectInputStream {
        private ClassLoader classLoader;

        LoaderInputStream(InputStream in, ClassLoader cl) throws IOException {
            super(in);
            classLoader = cl;
        }

        protected Class<?> resolveClass(ObjectStreamClass desc) throws
                IOException, ClassNotFoundException {
            try {
                // %%% Should use Class.forName(desc.getName(), false, classLoader);
                // except we can't because that is only available on JDK1.2
                return classLoader.loadClass(desc.getName());
            } catch (ClassNotFoundException e) {
                return super.resolveClass(desc);
            }
        }

         protected Class<?> resolveProxyClass(String[] interfaces) throws
                IOException, ClassNotFoundException {
             ClassLoader nonPublicLoader = null;
             boolean hasNonPublicInterface = false;

             // define proxy in class loader of non-public interface(s), if any
             Class<?>[] classObjs = new Class<?>[interfaces.length];
             for (int i = 0; i < interfaces.length; i++) {
                 Class<?> cl = Class.forName(interfaces[i], false, classLoader);
                 if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
                     if (hasNonPublicInterface) {
                         if (nonPublicLoader != cl.getClassLoader()) {
                             throw new IllegalAccessError(
                                "conflicting non-public interface class loaders");
                         }
                     } else {
                         nonPublicLoader = cl.getClassLoader();
                         hasNonPublicInterface = true;
                     }
                 }
                 classObjs[i] = cl;
             }
             try {
                 @SuppressWarnings("deprecation")
                 Class<?> proxyClass = Proxy.getProxyClass(hasNonPublicInterface ?
                        nonPublicLoader : classLoader, classObjs);
                 return proxyClass;
             } catch (IllegalArgumentException e) {
                 throw new ClassNotFoundException(null, e);
             }
         }

     }
}
