/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.wildfly.iiop.openjdk.csiv2;

import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.omg.CSIIOP.CompoundSecMech;
import org.omg.CSIIOP.CompoundSecMechList;
import org.omg.CSIIOP.CompoundSecMechListHelper;
import org.omg.CSIIOP.Confidentiality;
import org.omg.CSIIOP.DetectMisordering;
import org.omg.CSIIOP.DetectReplay;
import org.omg.CSIIOP.EstablishTrustInClient;
import org.omg.CSIIOP.EstablishTrustInTarget;
import org.omg.CSIIOP.Integrity;
import org.omg.CSIIOP.TAG_TLS_SEC_TRANS;
import org.omg.CSIIOP.TLS_SEC_TRANS;
import org.omg.CSIIOP.TLS_SEC_TRANSHelper;
import org.omg.CSIIOP.TransportAddress;
import org.omg.IOP.TAG_ALTERNATE_IIOP_ADDRESS;
import org.omg.IOP.TAG_CSI_SEC_MECH_LIST;
import org.omg.IOP.TaggedComponent;
import org.omg.SSLIOP.SSL;
import org.omg.SSLIOP.SSLHelper;
import org.omg.SSLIOP.TAG_SSL_SEC_TRANS;
import org.wildfly.iiop.openjdk.Constants;

import com.sun.corba.se.impl.encoding.CDRInputStream;
import com.sun.corba.se.impl.encoding.EncapsInputStream;
import com.sun.corba.se.spi.ior.IOR;
import com.sun.corba.se.spi.ior.iiop.AlternateIIOPAddressComponent;
import com.sun.corba.se.spi.ior.iiop.IIOPAddress;
import com.sun.corba.se.spi.ior.iiop.IIOPProfileTemplate;
import com.sun.corba.se.spi.orb.ORB;
import com.sun.corba.se.spi.transport.IORToSocketInfo;
import com.sun.corba.se.spi.transport.SocketInfo;
import org.wildfly.iiop.openjdk.logging.IIOPLogger;

import static java.security.AccessController.doPrivileged;

/**
 * <p>
 * Implements an {@code com.sun.corba.se.spi.transport.IORToSocketInfo} which creates SocketInfo based on IOR contents. If CSIv2
 * tagged component is present and it contains {@code org.omg.CSIIOP.TLS_SEC_TRANS} security mechanism then SSL socket is
 * created.
 * </p>
 *
 * @author <a href="mailto:tadamski@redhat.com">Tomasz Adamski</a>
 */

public class CSIV2IORToSocketInfo implements IORToSocketInfo {

    private static boolean clientRequiresSsl;

    public static void setClientRequiresSSL(final boolean clientRequiresSSL) {
        CSIV2IORToSocketInfo.clientRequiresSsl = clientRequiresSSL;
    }

    public List getSocketInfo(IOR ior) {

        List result = new ArrayList();

        IIOPProfileTemplate iiopProfileTemplate = (IIOPProfileTemplate) ior.getProfile().getTaggedProfileTemplate();
        IIOPAddress primary = iiopProfileTemplate.getPrimaryAddress();
        String hostname = primary.getHost().toLowerCase(Locale.ENGLISH);
        int primaryPort = primary.getPort();

        // NOTE: we could check for 0 (i.e., CSIv2) but, for a
        // non-CSIv2-configured client ORB talking to a CSIv2 configured
        // server ORB you might end up with an empty contact info list
        // which would then report a failure which would not be as
        // instructive as leaving a ContactInfo with a 0 port in the list.
        SocketInfo socketInfo;

        TransportAddress sslAddress = selectSSLTransportAddress(ior);
        SSL ssl = getSSL(ior);
        if (sslAddress != null) {
            socketInfo = createSSLSocketInfo(hostname, sslAddress.port);
        } else if (ssl != null) {
            socketInfo = createSSLSocketInfo(hostname, ssl.port);
        } else {
            // FIXME not all corba object export ssl port
            // if (clientRequiresSsl) {
            // throw new RuntimeException("Client requires SSL but target does not support it");
            // }
            socketInfo = createSocketInfo(hostname, primaryPort);
        }
        result.add(socketInfo);

        addAlternateSocketInfos(iiopProfileTemplate, result);

        return result;
    }

    private SSL getSSL(IOR ior){
        Iterator iter = ior.getProfile().getTaggedProfileTemplate().iteratorById(TAG_SSL_SEC_TRANS.value);
        if(!iter.hasNext()){
            return null;
        }
        ORB orb = ior.getORB();
        TaggedComponent compList = ((com.sun.corba.se.spi.ior.TaggedComponent) iter.next()).getIOPComponent(orb);
        CDRInputStream in = doPrivileged(new PrivilegedAction<CDRInputStream>() {
            @Override
            public CDRInputStream run() {
                return new EncapsInputStream(orb, compList.component_data, compList.component_data.length);
            }
        });
        in.consumeEndian();

        SSL ssl = SSLHelper.read(in);
        boolean targetRequiresSsl = ssl.target_requires > 0;
        boolean targetSupportsSsl = ssl.target_supports >0;
        if(!targetSupportsSsl && clientRequiresSsl){
            throw IIOPLogger.ROOT_LOGGER.serverDoesNotSupportSsl();
        }
        return targetSupportsSsl && (targetRequiresSsl || clientRequiresSsl) ? ssl : null;
    }

    private TransportAddress selectSSLTransportAddress(IOR ior) {
        CompoundSecMechList compoundSecMechList = readCompoundSecMechList(ior);
        if (compoundSecMechList != null) {
            for (CompoundSecMech mech : compoundSecMechList.mechanism_list) {
                TLS_SEC_TRANS sslMech = extractTlsSecTrans(ior, mech);
                if (sslMech == null) {
                    continue;
                }
                boolean targetSupportsSsl = checkSSL(sslMech.target_supports);
                boolean targetRequiresSsl = checkSSL(sslMech.target_requires);
                if(!targetSupportsSsl && clientRequiresSsl){
                    throw IIOPLogger.ROOT_LOGGER.serverDoesNotSupportSsl();
                }
                if (targetSupportsSsl && (targetRequiresSsl || clientRequiresSsl)) {
                    return extractAddress(sslMech);
                }
            }
        }
        return null;
    }

    private boolean checkSSL(int options) {
        return (options & (Integrity.value | Confidentiality.value | DetectReplay.value | DetectMisordering.value
                | EstablishTrustInTarget.value | EstablishTrustInClient.value)) != 0;
    }

    private CompoundSecMechList readCompoundSecMechList(IOR ior) {
        Iterator iter = ior.getProfile().getTaggedProfileTemplate().iteratorById(TAG_CSI_SEC_MECH_LIST.value);
        if (!iter.hasNext()) {
            return null;
        }
        ORB orb = ior.getORB();
        TaggedComponent compList = ((com.sun.corba.se.spi.ior.TaggedComponent) iter.next()).getIOPComponent(orb);
        CDRInputStream in = doPrivileged(new PrivilegedAction<CDRInputStream>() {
            @Override
            public CDRInputStream run() {
                return new EncapsInputStream(orb, compList.component_data, compList.component_data.length);
            }
        });
        in.consumeEndian();
        return CompoundSecMechListHelper.read(in);
    }

    private TLS_SEC_TRANS extractTlsSecTrans(IOR ior, CompoundSecMech mech) {
        TaggedComponent comp = mech.transport_mech;
        if (comp.tag != TAG_TLS_SEC_TRANS.value) {
            return null;
        }
        ORB orb = ior.getORB();
        CDRInputStream in = doPrivileged(new PrivilegedAction<CDRInputStream>() {
            @Override
            public CDRInputStream run() {
                return new EncapsInputStream(orb, comp.component_data, comp.component_data.length);
            }
        });
        in.consumeEndian();
        return TLS_SEC_TRANSHelper.read(in);
    }

    private TransportAddress extractAddress(TLS_SEC_TRANS sslMech) {
        if (sslMech.addresses.length == 0) {
            return null;
        }
        return sslMech.addresses[0];
    }

    private void addAlternateSocketInfos(IIOPProfileTemplate iiopProfileTemplate, final List result) {
        Iterator iterator = iiopProfileTemplate.iteratorById(TAG_ALTERNATE_IIOP_ADDRESS.value);

        while (iterator.hasNext()) {
            AlternateIIOPAddressComponent alternate = (AlternateIIOPAddressComponent) iterator.next();
            String hostname = alternate.getAddress().getHost().toLowerCase(Locale.ENGLISH);
            int port = alternate.getAddress().getPort();
            SocketInfo socketInfo = createSocketInfo(hostname, port);
            result.add(socketInfo);
        }
    }

    private SocketInfo createSocketInfo(final String hostname, final int port) {
        return new SocketInfo() {
            public String getType() {
                return SocketInfo.IIOP_CLEAR_TEXT;
            }

            public String getHost() {
                return hostname;
            }

            public int getPort() {
                return port;
            }
        };
    }

    private SocketInfo createSSLSocketInfo(final String hostname, final int port) {
        return new SocketInfo() {
            public String getType() {
                return Constants.SSL_SOCKET_TYPE;
            }

            public String getHost() {
                return hostname;
            }

            public int getPort() {
                return port;
            }
        };
    }
}
