/*
 * Copyright (c) 1996, 2012, 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.rmi.transport.proxy;

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.rmi.server.LogStream;
import java.rmi.server.RMISocketFactory;
import sun.rmi.runtime.Log;
import sun.rmi.runtime.NewThreadAction;
import sun.security.action.GetBooleanAction;
import sun.security.action.GetLongAction;

/**
 * RMIMasterSocketFactory attempts to create a socket connection to the
 * specified host using successively less efficient mechanisms
 * until one succeeds.  If the host is successfully connected to,
 * the factory for the successful mechanism is stored in an internal
 * hash table keyed by the host name, so that future attempts to
 * connect to the same host will automatically use the same
 * mechanism.
 */
public class RMIMasterSocketFactory extends RMISocketFactory {

    /** "proxy" package log level */
    static int logLevel = LogStream.parseLevel(getLogLevel());

    private static String getLogLevel() {
        return java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction("sun.rmi.transport.proxy.logLevel"));
    }

    /* proxy package log */
    static final Log proxyLog =
        Log.getLog("sun.rmi.transport.tcp.proxy",
                   "transport", RMIMasterSocketFactory.logLevel);

    /** timeout for attemping direct socket connections */
    private static long connectTimeout = getConnectTimeout();

    private static long getConnectTimeout() {
        return java.security.AccessController.doPrivileged(
                new GetLongAction("sun.rmi.transport.proxy.connectTimeout",
                              15000)).longValue(); // default: 15 seconds
    }

    /** whether to fallback to HTTP on general connect failures */
    private static final boolean eagerHttpFallback =
        java.security.AccessController.doPrivileged(new GetBooleanAction(
            "sun.rmi.transport.proxy.eagerHttpFallback")).booleanValue();

    /** table of hosts successfully connected to and the factory used */
    private Hashtable<String, RMISocketFactory> successTable =
        new Hashtable<>();

    /** maximum number of hosts to remember successful connection to */
    private static final int MaxRememberedHosts = 64;

    /** list of the hosts in successTable in initial connection order */
    private Vector<String> hostList = new Vector<>(MaxRememberedHosts);

    /** default factory for initial use for direct socket connection */
    protected RMISocketFactory initialFactory = new RMIDirectSocketFactory();

    /** ordered list of factories to try as alternate connection
      * mechanisms if a direct socket connections fails */
    protected Vector<RMISocketFactory> altFactoryList;

    /**
     * Create a RMIMasterSocketFactory object.  Establish order of
     * connection mechanisms to attempt on createSocket, if a direct
     * socket connection fails.
     */
    public RMIMasterSocketFactory() {
        altFactoryList = new Vector<>(2);
        boolean setFactories = false;

        try {
            String proxyHost;
            proxyHost = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction("http.proxyHost"));

            if (proxyHost == null)
                proxyHost = java.security.AccessController.doPrivileged(
                    new sun.security.action.GetPropertyAction("proxyHost"));

            Boolean tmp = java.security.AccessController.doPrivileged(
                new sun.security.action.GetBooleanAction("java.rmi.server.disableHttp"));

            if (!tmp.booleanValue() &&
                (proxyHost != null && proxyHost.length() > 0)) {
                setFactories = true;
            }
        } catch (Exception e) {
            // unable to obtain the properties, so assume default behavior.
            setFactories = true;
        }

        if (setFactories) {
            altFactoryList.addElement(new RMIHttpToPortSocketFactory());
            altFactoryList.addElement(new RMIHttpToCGISocketFactory());
        }
    }

    /**
     * Create a new client socket.  If we remember connecting to this host
     * successfully before, then use the same factory again.  Otherwise,
     * try using a direct socket connection and then the alternate factories
     * in the order specified in altFactoryList.
     */
    public Socket createSocket(String host, int port)
        throws IOException
    {
        if (proxyLog.isLoggable(Log.BRIEF)) {
            proxyLog.log(Log.BRIEF, "host: " + host + ", port: " + port);
        }

        /*
         * If we don't have any alternate factories to consult, short circuit
         * the fallback procedure and delegate to the initial factory.
         */
        if (altFactoryList.size() == 0) {
            return initialFactory.createSocket(host, port);
        }

        RMISocketFactory factory;

        /*
         * If we remember successfully connecting to this host before,
         * use the same factory.
         */
        factory = successTable.get(host);
        if (factory != null) {
            if (proxyLog.isLoggable(Log.BRIEF)) {
                proxyLog.log(Log.BRIEF,
                    "previously successful factory found: " + factory);
            }
            return factory.createSocket(host, port);
        }

        /*
         * Next, try a direct socket connection.  Open socket in another
         * thread and only wait for specified timeout, in case the socket
         * would otherwise spend minutes trying an unreachable host.
         */
        Socket initialSocket = null;
        Socket fallbackSocket = null;
        final AsyncConnector connector =
            new AsyncConnector(initialFactory, host, port,
                AccessController.getContext());
                // connection must be attempted with
                // this thread's access control context
        IOException initialFailure = null;

        try {
            synchronized (connector) {

                Thread t = java.security.AccessController.doPrivileged(
                    new NewThreadAction(connector, "AsyncConnector", true));
                t.start();

                try {
                    long now = System.currentTimeMillis();
                    long deadline = now + connectTimeout;
                    do {
                        connector.wait(deadline - now);
                        initialSocket = checkConnector(connector);
                        if (initialSocket != null)
                            break;
                        now = System.currentTimeMillis();
                    } while (now < deadline);
                } catch (InterruptedException e) {
                    throw new InterruptedIOException(
                        "interrupted while waiting for connector");
                }
            }

            // assume no route to host (for now) if no connection yet
            if (initialSocket == null)
                throw new NoRouteToHostException(
                    "connect timed out: " + host);

            proxyLog.log(Log.BRIEF, "direct socket connection successful");

            return initialSocket;

        } catch (UnknownHostException | NoRouteToHostException e) {
            initialFailure = e;
        } catch (SocketException e) {
            if (eagerHttpFallback) {
                initialFailure = e;
            } else {
                throw e;
            }
        } finally {
            if (initialFailure != null) {

                if (proxyLog.isLoggable(Log.BRIEF)) {
                    proxyLog.log(Log.BRIEF,
                        "direct socket connection failed: ", initialFailure);
                }

                // Finally, try any alternate connection mechanisms.
                for (int i = 0; i < altFactoryList.size(); ++ i) {
                    factory = altFactoryList.elementAt(i);
                    if (proxyLog.isLoggable(Log.BRIEF)) {
                        proxyLog.log(Log.BRIEF,
                            "trying with factory: " + factory);
                    }
                    try (Socket testSocket =
                            factory.createSocket(host, port)) {
                        // For HTTP connections, the output (POST request) must
                        // be sent before we verify a successful connection.
                        // So, sacrifice a socket for the sake of testing...
                        // The following sequence should verify a successful
                        // HTTP connection if no IOException is thrown.
                        InputStream in = testSocket.getInputStream();
                        int b = in.read(); // probably -1 for EOF...
                    } catch (IOException ex) {
                        if (proxyLog.isLoggable(Log.BRIEF)) {
                            proxyLog.log(Log.BRIEF, "factory failed: ", ex);
                        }

                        continue;
                    }
                    proxyLog.log(Log.BRIEF, "factory succeeded");

                    // factory succeeded, open new socket for caller's use
                    try {
                        fallbackSocket = factory.createSocket(host, port);
                    } catch (IOException ex) {  // if it fails 2nd time,
                    }                           // just give up
                    break;
                }
            }
        }

        synchronized (successTable) {
            try {
                // check once again to see if direct connection succeeded
                synchronized (connector) {
                    initialSocket = checkConnector(connector);
                }
                if (initialSocket != null) {
                    // if we had made another one as well, clean it up...
                    if (fallbackSocket != null)
                        fallbackSocket.close();
                    return initialSocket;
                }
                // if connector ever does get socket, it won't be used
                connector.notUsed();
            } catch (UnknownHostException | NoRouteToHostException e) {
                initialFailure = e;
            } catch (SocketException e) {
                if (eagerHttpFallback) {
                    initialFailure = e;
                } else {
                    throw e;
                }
            }
            // if we had found an alternate mechanism, go and use it
            if (fallbackSocket != null) {
                // remember this successful host/factory pair
                rememberFactory(host, factory);
                return fallbackSocket;
            }
            throw initialFailure;
        }
    }

    /**
     * Remember a successful factory for connecting to host.
     * Currently, excess hosts are removed from the remembered list
     * using a Least Recently Created strategy.
     */
    void rememberFactory(String host, RMISocketFactory factory) {
        synchronized (successTable) {
            while (hostList.size() >= MaxRememberedHosts) {
                successTable.remove(hostList.elementAt(0));
                hostList.removeElementAt(0);
            }
            hostList.addElement(host);
            successTable.put(host, factory);
        }
    }

    /**
     * Check if an AsyncConnector succeeded.  If not, return socket
     * given to fall back to.
     */
    Socket checkConnector(AsyncConnector connector)
        throws IOException
    {
        Exception e = connector.getException();
        if (e != null) {
            e.fillInStackTrace();
            /*
             * The AsyncConnector implementation guaranteed that the exception
             * will be either an IOException or a RuntimeException, and we can
             * only throw one of those, so convince that compiler that it must
             * be one of those.
             */
            if (e instanceof IOException) {
                throw (IOException) e;
            } else if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            } else {
                throw new Error("internal error: " +
                    "unexpected checked exception: " + e.toString());
            }
        }
        return connector.getSocket();
    }

    /**
     * Create a new server socket.
     */
    public ServerSocket createServerSocket(int port) throws IOException {
        //return new HttpAwareServerSocket(port);
        return initialFactory.createServerSocket(port);
    }


    /**
     * AsyncConnector is used by RMIMasterSocketFactory to attempt socket
     * connections on a separate thread.  This allows RMIMasterSocketFactory
     * to control how long it will wait for the connection to succeed.
     */
    private class AsyncConnector implements Runnable {

        /** what factory to use to attempt connection */
        private RMISocketFactory factory;

        /** the host to connect to */
        private String host;

        /** the port to connect to */
        private int port;

        /** access control context to attempt connection within */
        private AccessControlContext acc;

        /** exception that occurred during connection, if any */
        private Exception exception = null;

        /** the connected socket, if successful */
        private Socket socket = null;

        /** socket should be closed after created, if ever */
        private boolean cleanUp = false;

        /**
         * Create a new asynchronous connector object.
         */
        AsyncConnector(RMISocketFactory factory, String host, int port,
                       AccessControlContext acc)
        {
            this.factory = factory;
            this.host    = host;
            this.port    = port;
            this.acc     = acc;
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkConnect(host, port);
            }
        }

        /**
         * Attempt socket connection in separate thread.  If successful,
         * notify master waiting,
         */
        public void run() {
            try {
                /*
                 * Using the privileges of the thread that wants to make the
                 * connection is tempting, but it will fail with applets with
                 * the current applet security manager because the applet
                 * network connection policy is not captured in the permission
                 * framework of the access control context we have.
                 *
                 * java.security.AccessController.beginPrivileged(acc);
                 */
                try {
                    Socket temp = factory.createSocket(host, port);
                    synchronized (this) {
                        socket = temp;
                        notify();
                    }
                    rememberFactory(host, factory);
                    synchronized (this) {
                        if (cleanUp)
                          try {
                              socket.close();
                          } catch (IOException e) {
                          }
                    }
                } catch (Exception e) {
                    /*
                     * Note that the only exceptions which could actually have
                     * occurred here are IOException or RuntimeException.
                     */
                    synchronized (this) {
                        exception = e;
                        notify();
                    }
                }
            } finally {
                /*
                 * See above comments for matching beginPrivileged() call that
                 * is also commented out.
                 *
                 * java.security.AccessController.endPrivileged();
                 */
            }
        }

        /**
         * Get exception that occurred during connection attempt, if any.
         * In the current implementation, this is guaranteed to be either
         * an IOException or a RuntimeException.
         */
        private synchronized Exception getException() {
            return exception;
        }

        /**
         * Get successful socket, if any.
         */
        private synchronized Socket getSocket() {
            return socket;
        }

        /**
         * Note that this connector's socket, if ever successfully created,
         * will not be used, so it should be cleaned up quickly
         */
        synchronized void notUsed() {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                }
            }
            cleanUp = true;
        }
    }
}
