package org.jacorb.orb.iiop;

/*
 *        JacORB - a free Java ORB
 *
 *   Copyright (C) 1999-2014 Gerald Brose / The JacORB Team.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library 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
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import javax.net.ssl.SSLSocket;
import org.jacorb.config.Configuration;
import org.jacorb.config.ConfigurationException;
import org.jacorb.orb.BasicAdapter;
import org.jacorb.orb.factory.ServerSocketFactory;
import org.jacorb.orb.factory.SocketFactoryManager;
import org.jacorb.orb.listener.AcceptorExceptionEvent;
import org.jacorb.orb.listener.AcceptorExceptionListener;
import org.jacorb.orb.listener.DefaultAcceptorExceptionListener;
import org.jacorb.orb.listener.SSLListenerUtil;
import org.jacorb.orb.listener.TCPConnectionEvent;
import org.jacorb.orb.listener.TCPConnectionListener;
import org.omg.CORBA.NO_RESOURCES;
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.ETF.Connection;
import org.omg.SSLIOP.SSL;
import org.omg.SSLIOP.SSLHelper;
import org.omg.SSLIOP.TAG_SSL_SEC_TRANS;

/**
 * @author Andre Spiegel
 */
public class IIOPListener
    extends org.jacorb.orb.etf.ListenerBase
{
    /** the maximum set of security options supported by the SSL mechanism */
    private static final int MAX_SSL_OPTIONS = Integrity.value |
                                               Confidentiality.value |
                                               DetectReplay.value |
                                               DetectMisordering.value |
                                               EstablishTrustInTarget.value |
                                               EstablishTrustInClient.value;

    /**  the minimum set of security options supported by the SSL mechanism
     *   which cannot be turned off, so they are always supported
     */
    private static final int MIN_SSL_OPTIONS = Integrity.value |
                                               DetectReplay.value |
                                               DetectMisordering.value;


    private SocketFactoryManager socketFactoryManager = null;

    private SSLAcceptor sslAcceptor = null;
    private LoopbackAcceptor loopbackAcceptor ;

    private boolean supportSSL = false;
    private int serverTimeout = 0;
    private IIOPAddress address = null;
    private IIOPAddress sslAddress = null;
    private int target_supports = 0;
    private int target_requires = 0;
    private boolean generateSSLComponents = true;

    public IIOPListener()
    {
        super();
    }

    public void configure(Configuration config)
        throws ConfigurationException
    {
        super.configure(config);

        socketFactoryManager = orb.getTransportManager().getSocketFactoryManager();
        if (listenEndpoint == null)
        {
            throw new org.omg.CORBA.INITIALIZE
                ("listenEndpoint may not be null");
        }
        if (listenEndpoint.getSSLAddress() != null)
        {
            sslAddress = (IIOPAddress) listenEndpoint.getSSLAddress();
        }
        if (listenEndpoint.getAddress() != null)
        {
            address = (IIOPAddress) listenEndpoint.getAddress();
        }

        if (address != null)
        {
            address.configure (configuration);
        }

        if (sslAddress != null)
        {
            sslAddress.configure (configuration);
        }

        serverTimeout =
            configuration.getAttributeAsInteger("jacorb.connection.server.timeout",0);

        supportSSL =
            configuration.getAttributeAsBoolean("jacorb.security.support_ssl", false);

        target_supports =
           configuration.getAttributeAsInteger("jacorb.security.ssl.server.supported_options", 0x20, 16); // 16 is the base as we take the string value as hex!

        // make sure that the minimum options are always in the set of supported options
        target_supports |= MIN_SSL_OPTIONS;

        target_requires =
           configuration.getAttributeAsInteger("jacorb.security.ssl.server.required_options", 0, 16);


        generateSSLComponents =
            configuration.getAttributeAsBoolean("jacorb.security.ssl_components_added_by_ior_interceptor", true);


        if (!isSSLRequired() ||
            configuration.getAttributeAsBoolean("jacorb.security.ssl.always_open_unsecured_address", false))
        {
            acceptor = new Acceptor("ServerSocketListener");
            ((Acceptor)acceptor).init();
        }

        if (supportSSL)
        {
            sslAcceptor = new SSLAcceptor();
            sslAcceptor.init();
        }

        profile = createAddressProfile();

        if (configuration.getAttributeAsBoolean("jacorb.iiop.enable_loopback", true))
        {
            loopbackAcceptor = new LoopbackAcceptor();
        }

    }


    /**
     * {@inheritDoc}
     */
    public void listen()
    {
        super.listen();

        if (sslAcceptor != null)
        {
            sslAcceptor.start();
        }

        if (loopbackAcceptor != null)
        {
            loopbackAcceptor.start();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void destroy()
    {
        if (loopbackAcceptor != null)
        {
            loopbackAcceptor.terminate();
        }

        if (sslAcceptor != null)
        {
            sslAcceptor.terminate();
        }

        super.destroy();
    }

    public void renewSSLServerSocket()
    {
        if (sslAcceptor != null)
        {
            sslAcceptor.renewSSLServerSocket();
        }
    }

    // internal methods below this line

    /**
     * Returns true if this Listener should support SSL connections.
     */
    private boolean isSSLSupported()
    {
        return  supportSSL;
    }

    /**
     * Returns true if this Listener should <i>require</i> SSL, and not
     * offer plain connections.
     */
    private boolean isSSLRequired()
    {
        if (isSSLSupported())
        {
            // the following is used as a bit mask to check if any SSL
            // options are required
            return ((target_requires & MAX_SSL_OPTIONS ) != 0);
        }
        return false;
    }

    /**
     * Creates a new IIOPProfile that describes this transport address.
     */
    private IIOPProfile createAddressProfile()
        throws ConfigurationException
    {
        IIOPProfile result = null;

        if (acceptor != null)
        {
            IIOPAddress serverAddress = ((Acceptor)acceptor).getLocalAddress();

            if (address.getPort() == 0)
            {
                address.setPort(serverAddress.getPort());
            }
            if (logger.isDebugEnabled())
            {
                logger.debug ("IIOPAddress using port " + address.getPort());
            }

            if (address.getConfiguredHost() == null)
            {
                address.setHostInetAddress (serverAddress.getConfiguredHost());
            }
            else
            {
                /**
                 * In case some users set the hostname to all zeroes
                 * ("0.0.0.0") which could be a wildcard host if it is not
                 * mapped to a loop-back IP.
                 */
                address.setWildcardHost (serverAddress.isWildcard());
            }

            result = new IIOPProfile(serverAddress, null, orb.getGIOPMinorVersion());
            result.configure(configuration);

            // Add all wildcard addresses to the list of alternative addresses
            if (serverAddress.isWildcard())
            {
                result.addAllWildcardAddresses(configuration);
            }
        }
        else if (sslAcceptor == null)
        {
            throw new org.omg.CORBA.INITIALIZE
                ("no acceptors found, cannot create address profile");
        }

        if (sslAcceptor != null)
        {
            IIOPAddress serverAddress = ((SSLAcceptor)sslAcceptor).getLocalAddress();
            if (sslAddress.getPort() == 0)
            {
                sslAddress.setPort(serverAddress.getPort());
            }

            if (logger.isDebugEnabled())
            {
                logger.debug ("IIOPAddress using ssl port " + sslAddress.getPort());
            }

            if (sslAddress.getConfiguredHost() == null)
            {
                sslAddress.setHostInetAddress (serverAddress.getConfiguredHost());
            }
            else
            {
                sslAddress.setWildcardHost (serverAddress.isWildcard());
            }

            // This means a non-ssl profile hasn't been created, so create it here.
            if (result == null)
            {
                IIOPAddress tempAddress = ((SSLAcceptor)sslAcceptor).getLocalAddress();
                tempAddress.setPort(0);
                if (address != null && address.getPort() != 0)
                {
                    tempAddress.setPort(address.getPort());
                }
                result = new IIOPProfile(tempAddress, null, orb.getGIOPMinorVersion());
                result.configure(configuration);

                // Add all wildcard addresses to the list of alternative addresses
                if (tempAddress.isWildcard())
                {
                    result.addAllWildcardAddresses(configuration);
                }
            }

            if (generateSSLComponents)
            {
                result.addComponent (TAG_SSL_SEC_TRANS.value,
                                  createSSL(), SSLHelper.class);
            }
        }

        return result;
    }

    private SSL createSSL()
    {
        return new SSL
        (
            (short)target_supports,
            (short)target_requires,
            (short)sslAcceptor.getLocalAddress().getPort()
        );
    }

    /**
     * Creates an ETF connection around a live socket and passes it
     * up to the ORB, using either the upcall mechanism or the
     * polling mechanism.
     */
    private void deliverConnection (Socket socket, boolean isSSL)
    {
        Connection result = null;
        try
        {
            result = createServerConnection(socket, isSSL);
        }
        catch (IOException ex)
        {
            if (logger.isErrorEnabled())
            {
                logger.error("Could not create connection from socket: ", ex);
            }
            return;
        }

        deliverConnection(result);
    }

    /**
     * Template method to create a server-side ETF Connection.
     * This can be overridden by subclasses to pass a different
     * kind of Connection up to the ORB.
     */
    protected Connection createServerConnection (Socket socket,
                                                 boolean is_ssl)
        throws IOException
    {
        final TCPConnectionListener tcpListener = socketFactoryManager.getTCPListener();
        ServerIIOPConnection result = new ServerIIOPConnection(socket, is_ssl, tcpListener);

        if (tcpListener.isListenerEnabled())
        {
            tcpListener.connectionOpened
            (
                    new TCPConnectionEvent
                    (
                            result,
                            socket.getInetAddress().getHostAddress(),
                            socket.getPort(),
                            socket.getLocalPort(),
                            getLocalhost()
                    )
            );
        }

        try
        {
            result.configure(configuration);
        }
        catch( ConfigurationException ce )
        {
            throw new org.omg.CORBA.INTERNAL("ConfigurationException: " + ce.toString());
        }
        return result;
    }

    // Acceptor classes below this line

    private String getLocalhost()
    {
        return IIOPAddress.getLocalHostAddress (logger);
    }

    /*
     * Acceptor class
     */
    public class Acceptor
        extends org.jacorb.orb.etf.ListenerBase.Acceptor
    {
        private final Object runSync = new Object();
        private final boolean keepAlive;
        private final IIOPAddress addressToUse;
        protected final int soTimeout;
        protected final boolean reuseAddress;
        protected ServerSocket serverSocket;
        protected String info = "";

        protected boolean terminated = false;

        /**
         * <code>acceptorExceptionListener</code> is listener to be notified
         * of terminal failures by the acceptor.
         */
        private final AcceptorExceptionListener acceptorExceptionListener;

        /**
         * <code>firstPass</code> stores whether we have already done
         * one pass in the run method i.e. have accepted one socket.
         */
        private boolean firstPass;

        protected Acceptor(String name, IIOPAddress target) throws ConfigurationException
        {
            super();
            // initialization deferred to init() method due to JDK bug
            setDaemon(true);
            setName(name);

            keepAlive = configuration.getAttributeAsBoolean("jacorb.connection.server.keepalive", false);
            soTimeout = configuration.getAttributeAsInteger("jacorb.listener.server_socket_timeout", 0);
            reuseAddress = configuration.getAttributeAsBoolean("jacorb.connection.server.reuse_address", false);

            try
            {
                acceptorExceptionListener = (AcceptorExceptionListener)configuration.getAttributeAsObject("jacorb.acceptor_exception_listener", DefaultAcceptorExceptionListener.class.getName());
            }
            catch (ConfigurationException e)
            {
                logger.error("couldn't create a AcceptorExceptionListener", e);
                throw new IllegalArgumentException("wrong configuration: " + e);
            }

            addressToUse = target;
        }

        protected Acceptor(String name) throws ConfigurationException
        {
            this(name, address);
        }

        public void init()
        {
            serverSocket = createServerSocket();

            if( logger.isDebugEnabled() )
            {
                logger.debug( "Created " + info + "socket listener on " +
                              serverSocket.getInetAddress() + ":" + serverSocket.getLocalPort() );
            }
       }

        /**
         * template method that is invoked during the accept loop
         * before an incoming connection is accepted.
         */
        protected void beginAccept() throws InterruptedException
        {
            // empty to be overridden
        }

        /**
         * template method that is invoked during the accept loop
         * after an incoming connection was processed.
         */
        protected void endAccept()
        {
            // empty to be overridden
        }

        public final void run()
        {
            try
            {
                runLoop();
            }
            finally
            {
                if (!terminated)
                {
                    logger.error(info + "Listener is unexpectedly exiting. the ORB is in an non-functional state!");
                }
                else
                {
                    logger.debug(info + "Listener exiting");
                }
            }
        }

        public final void runLoop()
        {
            while(true)
            {
                try
                {
                    synchronized(runSync)
                    {
                        if (terminated)
                        {
                            return;
                        }
                    }

                    beginAccept();

                    try
                    {
                        Socket socket = serverSocket.accept();

                        if (terminated)
                        {
                            // JAC#439
                            // serverSocket.close() and interruption of the
                            // Listener thread seem not to reliable prevent
                            // the serverSocket from accepting further connection requests.
                            // so we might come here although the IIOPListener was already
                            // shutdown.
                            // TODO: prohably it would be better to use
                            // SocketChannel socketChannel = serverSocket.getChannel().accept();
                            // Socket socket = socketChannel.socket();
                            // for more reliability

                            if ( ! (socket instanceof SSLSocket) && ! socket.isClosed())
                            {
                                socket.shutdownOutput();
                            }
                            socket.close();

                            if (logger.isInfoEnabled())
                            {
                                logger.info("closed Socket " + socket + " as " + info + "ServerSocket was closed.");
                            }
                            return;
                        }

                        setup(socket);

                        try
                        {
                            deliverConnection(socket);
                            firstPass = true;
                        }
                        catch(NO_RESOURCES e)
                        {
                            // as no ServerMessageReceptor
                            // was created for this socket
                            // we'll at least close
                            // the socket

                            if ( ! (socket instanceof SSLSocket) && ! socket.isClosed())
                            {
                                socket.shutdownOutput();
                            }
                            socket.close();
                            throw e;
                        }
                    }
                    finally
                    {
                        endAccept();
                    }
                }
                catch (Exception e)
                {
                    synchronized(runSync)
                    {
                        handleExceptionInRunLoop(e, terminated);
                    }
                }
                catch(OutOfMemoryError e)
                {
                    logger.error("OutOfMemory", e);
                }
            }
        }

        /**
         * template method that is invoked when
         * an exception occurs during the run loop.
         */
        private void handleExceptionInRunLoop(Exception exception, boolean isTerminated)
        {
            if (!isTerminated)
            {
                logger.warn("unexpected exception in " + info + "Acceptor runloop", exception);
            }

            doHandleExceptionInRunLoop(exception, isTerminated);

            try
            {
                acceptorExceptionListener.exceptionCaught
                (
                    new AcceptorExceptionEvent
                    (
                        this,
                        ((BasicAdapter) up).getORB(),
                        exception
                    )
                );
            }
            catch(Exception e)
            {
                logger.error("error in Acceptor Exception Listener: " + acceptorExceptionListener + " while handling exception: " + exception, e);
            }
        }

        protected void doHandleExceptionInRunLoop(Exception exception, boolean isTerminated)
        {
            // empty to be overridden
        }

        /**
         * Terminates this Acceptor by closing the ServerSocket and interrupting
         * the run loop.
         */
        public void terminate()
        {
            synchronized(runSync)
            {
                terminated = true;
            }
            try
            {
                serverSocket.close();
            }
            catch (java.io.IOException e)
            {
                if (logger.isWarnEnabled())
                {
                    logger.warn("failed to close " + info + "ServerSocket", e);
                }
            }

            interrupt();
        }

        public IIOPAddress getLocalAddress()
        {
            IIOPAddress addr = new IIOPAddress
            (
                    serverSocket
            );

            if (configuration != null)
            {
                try
                {
                    addr.configure(configuration);
                }
                catch( ConfigurationException ce)
                {
                    if (logger.isWarnEnabled())
                    {
                        logger.warn("ConfigurationException", ce );
                    }
                }
            }
            return addr;
        }

        protected ServerSocket createServerSocket()
        {
            final InetAddress configuredHost = addressToUse.getConfiguredHost();
            final int port = addressToUse.getPort();

            return createServerSocket(configuredHost, port);
        }

        protected ServerSocket createServerSocket(final InetAddress host, final int port)
        {
            try
            {
                final ServerSocket result;

                if (host != null)
                {
                    /*
                     * A server socket is created for a selected host.
                     * In case the port is 0, a port will be assigned
                     * to be used as the listen port.
                     * The backlog is set to -1 so that the system
                     * default value of 50 will be assigned.
                     *
                     */
                     result = getServerSocketFactory().createServerSocket(port, -1, host);
                }
                else
                {
                    /*
                     * A server socket is created for a wildcard host to listen
                     * for requests on all known network interfaces' host addresses.
                     * In case the port is 0, a port will be assigned
                     * as the listen port.  The backlog will be set to
                     * the system default value of 50.
                     *
                     */
                    result = getServerSocketFactory().createServerSocket(port);
                }

                if (soTimeout > 0)
                {
                    result.setSoTimeout(soTimeout);
                }

                if (reuseAddress)
                {
                    result.setReuseAddress(true);
                }

                return result;
            }
            catch (IOException ex)
            {
                logger.warn("could not create " + info + "ServerSocket port: " + port + " host: " + host, ex);
                throw new org.omg.CORBA.INITIALIZE ("Could not create " + info + "ServerSocket (" + port + "): " + ex.toString());
            }
        }

        protected ServerSocketFactory getServerSocketFactory()
        {
            return socketFactoryManager.getServerSocketFactory();
        }

        /**
         * Template method that sets up the socket right after the
         * connection has been established.  Subclass implementations
         * may implement their own logic by overriding doSetup
         */
        protected final void setup(Socket socket)
            throws IOException
        {
             socket.setSoTimeout(serverTimeout);
             socket.setKeepAlive(keepAlive);
             socket.setTcpNoDelay(true);

             try
             {
                 SSLListenerUtil.addListener(orb, socket);
             }
             catch(Throwable t)
             {
                 logger.warn("unexpected exception in ssl listener", t);
             }

             doSetup(socket);
        }

        protected void doSetup(Socket socket)
        {
            // empty to be overridden
        }

        protected void deliverConnection(Socket socket)
        {
            IIOPListener.this.deliverConnection (socket, false);
        }

        /**
         * <code>getAcceptorSocketLoop</code> returns whether we have done
         * a socket accept. This is useful for the AcceptorExceptionListener
         * so it can determine for instance if the SSLException has been
         * thrown before any connections have been made or after x amount of
         * connections - this allows differentiation between initial
         * configuration failure and failure to connect to a single client.
         *
         * @return a <code>boolean</code> value
         */
        public boolean getAcceptorSocketLoop()
        {
            return firstPass;
        }
    } // end of Acceptor

    private class SSLAcceptor
        extends Acceptor
    {
        private final Object renewSocketSync = new Object();
        private boolean renewingSocket;
        private boolean blockedOnAccept;

        private SSLAcceptor() throws ConfigurationException
        {
            super("SSLServerSocketListener", sslAddress);
            info = "SSL";
        }

        protected ServerSocketFactory getServerSocketFactory()
        {
            return socketFactoryManager.getSSLServerSocketFactory();
        }
        protected void deliverConnection(Socket socket)
        {
            IIOPListener.this.deliverConnection (socket, true);
        }

        protected void beginAccept() throws InterruptedException
        {
            synchronized (renewSocketSync)
            {
                //wait 'til new socket has been created
                while (renewingSocket)
                {
                    renewSocketSync.wait();
                }

                //tell renewing thread we're busy
                blockedOnAccept = true;
            }
        }

        protected void endAccept()
        {
            synchronized (renewSocketSync)
            {
                //tell renewing thread we're done
                blockedOnAccept = false;
                renewSocketSync.notifyAll();
            }
        }

        protected void doHandleExceptionInRunLoop(Exception exception, boolean isTerminated)
        {
            // we are only interested in InterruptedExceptions here
            if (!(exception instanceof InterruptedException))
            {
                return;
            }

            // accept took too long
            if (soTimeout > 0)
            {
                return;
            }

            // Thread was interrupted
            if (isTerminated)
            {
                return;
            }

            logger.warn("InterruptedException should only occur if soTimeout > 0", exception);
        }

        /**
         * Replace an existing SSLServersocket by a new one (possibly using
         * new key material) that's opened on the same address/port
         * combination.
         */
        public void renewSSLServerSocket()
        {
            //remember old settings
            final InetAddress oldAddress = serverSocket.getInetAddress();
            final int oldPort = serverSocket.getLocalPort();

            try
            {
                synchronized (renewSocketSync)
                {
                    //tell listener we want to renew. Attention: this needs to
                    //be done prior to waiting, otherwise we may starve
                    renewingSocket = true;

                    //wait 'til listener isn't accepting anymore
                    while (blockedOnAccept)
                    {
                        try
                        {
                            renewSocketSync.wait();
                        }
                        catch(InterruptedException e)
                        {
                            // ignored
                        }
                    }
                }

                try
                {
                    //close old socket
                    serverSocket.close();
                }
                catch (Exception e)
                {
                    logger.warn("failed to close SSLServerSocket", e);
                }

                //create new ServerSocket with old host/port
                serverSocket = createServerSocket(oldAddress, oldPort);
            }
            finally
            {
                // leave critical section
                synchronized(renewSocketSync)
                {
                    //tell listener we're done
                    renewingSocket = false;
                    renewSocketSync.notifyAll();
                }
            }
        }
    }

    private class LoopbackAcceptor implements IIOPLoopback
    {
        private final IIOPAddress listenerAddress;
        private final IIOPAddress loopbackAddress;
        private final boolean isSSL;

        public LoopbackAcceptor()
        {
            final IIOPProfile iiopProfile = (IIOPProfile)IIOPListener.this.profile;
            listenerAddress = (IIOPAddress)iiopProfile.getAddress().copy();

            loopbackAddress = (IIOPAddress) listenerAddress.copy();
            loopbackAddress.setHostname("127.0.0.1");
            isSSL = iiopProfile.getSSL() != null;

            if (isSSL)
            {
                listenerAddress.setPort(iiopProfile.getSSLPort());
                loopbackAddress.setPort(iiopProfile.getSSLPort());
            }

            if( logger.isDebugEnabled() )
            {
                logger.debug("LoopbackAcceptor creates loopbackAddress using: "
                        + "<" + loopbackAddress.toString() + ">");
            }

        }

        public void start()
        {
            IIOPLoopbackRegistry.getRegistry().register(listenerAddress, this);
            IIOPLoopbackRegistry.getRegistry().register(loopbackAddress, this);
        }

        public void terminate()
        {
            IIOPLoopbackRegistry.getRegistry().unregister(listenerAddress);
            IIOPLoopbackRegistry.getRegistry().unregister(loopbackAddress);
        }

        public void initLoopback(final String connectionInfo,
                final IIOPLoopbackInputStream lis,
                final IIOPLoopbackOutputStream los)
        {
            final IIOPLoopbackConnection connection = new IIOPLoopbackConnection(lis, los)
            {
                {
                    connection_info = connectionInfo;
                }

                public boolean isSSL()
                {
                    return isSSL;
                }
            };

            try
            {
                connection.configure(configuration);
            }
            catch(ConfigurationException e)
            {
                throw new RuntimeException("should never happen", e);
            }
            deliverConnection(connection);
        }
    }
}
