/*
 * Copyright (c) 1996, 2019, 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.tcp;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.security.AccessController;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import sun.rmi.runtime.Log;
import sun.rmi.runtime.NewThreadAction;
import sun.rmi.transport.Channel;
import sun.rmi.transport.Endpoint;
import sun.rmi.transport.Target;
import sun.rmi.transport.Transport;
import sun.security.action.GetBooleanAction;
import sun.security.action.GetIntegerAction;
import sun.security.action.GetPropertyAction;

/**
 * TCPEndpoint represents some communication endpoint for an address
 * space (VM).
 *
 * @author Ann Wollrath
 */
public class TCPEndpoint implements Endpoint {
    /** IP address or host name */
    private String host;
    /** port number */
    private int port;
    /** custom client socket factory (null if not custom factory) */
    private final RMIClientSocketFactory csf;
    /** custom server socket factory (null if not custom factory) */
    private final RMIServerSocketFactory ssf;

    /** if local, the port number to listen on */
    private int listenPort = -1;
    /** if local, the transport object associated with this endpoint */
    private TCPTransport transport = null;

    /** the local host name */
    private static String localHost;
    /** true if real local host name is known yet */
    private static boolean localHostKnown;

    // this should be a *private* method since it is privileged
    private static int getInt(String name, int def) {
        return AccessController.doPrivileged(new GetIntegerAction(name, def));
    }

    // this should be a *private* method since it is privileged
    private static boolean getBoolean(String name) {
        return AccessController.doPrivileged(new GetBooleanAction(name));
    }

    /**
     * Returns the value of the java.rmi.server.hostname property.
     */
    private static String getHostnameProperty() {
        return AccessController.doPrivileged(
            new GetPropertyAction("java.rmi.server.hostname"));
    }

    /**
     * Find host name of local machine.  Property "java.rmi.server.hostname"
     * is used if set, so server administrator can compensate for the possible
     * inablility to get fully qualified host name from VM.
     */
    static {
        localHostKnown = true;
        localHost = getHostnameProperty();

        // could try querying CGI program here?
        if (localHost == null) {
            try {
                InetAddress localAddr = InetAddress.getLocalHost();
                byte[] raw = localAddr.getAddress();
                if ((raw[0] == 127) &&
                    (raw[1] ==   0) &&
                    (raw[2] ==   0) &&
                    (raw[3] ==   1)) {
                    localHostKnown = false;
                }

                /* if the user wishes to use a fully qualified domain
                 * name then attempt to find one.
                 */
                if (getBoolean("java.rmi.server.useLocalHostName")) {
                    localHost = FQDN.attemptFQDN(localAddr);
                } else {
                    /* default to using ip addresses, names will
                     * work across seperate domains.
                     */
                    localHost = localAddr.getHostAddress();
                }
            } catch (Exception e) {
                localHostKnown = false;
                localHost = null;
            }
        }

        if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
            TCPTransport.tcpLog.log(Log.BRIEF,
                "localHostKnown = " + localHostKnown +
                ", localHost = " + localHost);
        }
    }

    /** maps an endpoint key containing custom socket factories to
     * their own unique endpoint */
    // TBD: should this be a weak hash table?
    private static final
        Map<TCPEndpoint,LinkedList<TCPEndpoint>> localEndpoints =
        new HashMap<>();

    /**
     * Create an endpoint for a specified host and port.
     * This should not be used by external classes to create endpoints
     * for servers in this VM; use getLocalEndpoint instead.
     */
    public TCPEndpoint(String host, int port) {
        this(host, port, null, null);
    }

    /**
     * Create a custom socket factory endpoint for a specified host and port.
     * This should not be used by external classes to create endpoints
     * for servers in this VM; use getLocalEndpoint instead.
     */
    public TCPEndpoint(String host, int port, RMIClientSocketFactory csf,
                       RMIServerSocketFactory ssf)
    {
        if (host == null)
            host = "";
        this.host = host;
        this.port = port;
        this.csf = csf;
        this.ssf = ssf;
    }

    /**
     * Get an endpoint for the local address space on specified port.
     * If port number is 0, it returns shared default endpoint object
     * whose host name and port may or may not have been determined.
     */
    public static TCPEndpoint getLocalEndpoint(int port) {
        return getLocalEndpoint(port, null, null);
    }

    public static TCPEndpoint getLocalEndpoint(int port,
                                               RMIClientSocketFactory csf,
                                               RMIServerSocketFactory ssf)
    {
        /*
         * Find mapping for an endpoint key to the list of local unique
         * endpoints for this client/server socket factory pair (perhaps
         * null) for the specific port.
         */
        TCPEndpoint ep = null;

        synchronized (localEndpoints) {
            TCPEndpoint endpointKey = new TCPEndpoint(null, port, csf, ssf);
            LinkedList<TCPEndpoint> epList = localEndpoints.get(endpointKey);
            String localHost = resampleLocalHost();

            if (epList == null) {
                /*
                 * Create new endpoint list.
                 */
                ep = new TCPEndpoint(localHost, port, csf, ssf);
                epList = new LinkedList<TCPEndpoint>();
                epList.add(ep);
                ep.listenPort = port;
                ep.transport = new TCPTransport(epList);
                localEndpoints.put(endpointKey, epList);

                if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
                    TCPTransport.tcpLog.log(Log.BRIEF,
                        "created local endpoint for socket factory " + ssf +
                        " on port " + port);
                }
            } else {
                synchronized (epList) {
                    ep = epList.getLast();
                    String lastHost = ep.host;
                    int lastPort =  ep.port;
                    TCPTransport lastTransport = ep.transport;
                    // assert (localHost == null ^ lastHost != null)
                    if (localHost != null && !localHost.equals(lastHost)) {
                        /*
                         * Hostname has been updated; add updated endpoint
                         * to list.
                         */
                        if (lastPort != 0) {
                            /*
                             * Remove outdated endpoints only if the
                             * port has already been set on those endpoints.
                             */
                            epList.clear();
                        }
                        ep = new TCPEndpoint(localHost, lastPort, csf, ssf);
                        ep.listenPort = port;
                        ep.transport = lastTransport;
                        epList.add(ep);
                    }
                }
            }
        }

        return ep;
    }

    /**
     * Resamples the local hostname and returns the possibly-updated
     * local hostname.
     */
    private static String resampleLocalHost() {

        String hostnameProperty = getHostnameProperty();

        synchronized (localEndpoints) {
            // assert(localHostKnown ^ (localHost == null))

            if (hostnameProperty != null) {
                if (!localHostKnown) {
                    /*
                     * If the local hostname is unknown, update ALL
                     * existing endpoints with the new hostname.
                     */
                    setLocalHost(hostnameProperty);
                } else if (!hostnameProperty.equals(localHost)) {
                    /*
                     * Only update the localHost field for reference
                     * in future endpoint creation.
                     */
                    localHost = hostnameProperty;

                    if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
                        TCPTransport.tcpLog.log(Log.BRIEF,
                            "updated local hostname to: " + localHost);
                    }
                }
            }
            return localHost;
        }
    }

    /**
     * Set the local host name, if currently unknown.
     */
    static void setLocalHost(String host) {
        // assert (host != null)

        synchronized (localEndpoints) {
            /*
             * If host is not known, change the host field of ALL
             * the local endpoints.
             */
            if (!localHostKnown) {
                localHost = host;
                localHostKnown = true;

                if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
                    TCPTransport.tcpLog.log(Log.BRIEF,
                        "local host set to " + host);
                }
                for (LinkedList<TCPEndpoint> epList : localEndpoints.values())
                {
                    synchronized (epList) {
                        for (TCPEndpoint ep : epList) {
                            ep.host = host;
                        }
                    }
                }
            }
        }
    }

    /**
     * Set the port of the (shared) default endpoint object.
     * When first created, it contains port 0 because the transport
     * hasn't tried to listen to get assigned a port, or if listening
     * failed, a port hasn't been assigned from the server.
     */
    static void setDefaultPort(int port, RMIClientSocketFactory csf,
                               RMIServerSocketFactory ssf)
    {
        TCPEndpoint endpointKey = new TCPEndpoint(null, 0, csf, ssf);

        synchronized (localEndpoints) {
            LinkedList<TCPEndpoint> epList = localEndpoints.get(endpointKey);

            synchronized (epList) {
                int size = epList.size();
                TCPEndpoint lastEp = epList.getLast();

                for (TCPEndpoint ep : epList) {
                    ep.port = port;
                }
                if (size > 1) {
                    /*
                     * Remove all but the last element of the list
                     * (which contains the most recent hostname).
                     */
                    epList.clear();
                    epList.add(lastEp);
                }
            }

            /*
             * Allow future exports to use the actual bound port
             * explicitly (see 6269166).
             */
            TCPEndpoint newEndpointKey = new TCPEndpoint(null, port, csf, ssf);
            localEndpoints.put(newEndpointKey, epList);

            if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
                TCPTransport.tcpLog.log(Log.BRIEF,
                    "default port for server socket factory " + ssf +
                    " and client socket factory " + csf +
                    " set to " + port);
            }
        }
    }

    /**
     * Returns transport for making connections to remote endpoints;
     * (here, the default transport at port 0 is used).
     */
    public Transport getOutboundTransport() {
        TCPEndpoint localEndpoint = getLocalEndpoint(0, null, null);
        return localEndpoint.transport;
    }

    /**
     * Returns the current list of known transports.
     * The returned list is an unshared collection of Transports,
     * including all transports which may have channels to remote
     * endpoints.
     */
    private static Collection<TCPTransport> allKnownTransports() {
        // Loop through local endpoints, getting the transport of each one.
        Set<TCPTransport> s;
        synchronized (localEndpoints) {
            // presize s to number of localEndpoints
            s = new HashSet<TCPTransport>(localEndpoints.size());
            for (LinkedList<TCPEndpoint> epList : localEndpoints.values()) {
                /*
                 * Each local endpoint has its transport added to s.
                 * Note: the transport is the same for all endpoints
                 * in the list, so it is okay to pick any one of them.
                 */
                TCPEndpoint ep = epList.getFirst();
                s.add(ep.transport);
            }
        }
        return s;
    }

    /**
     * Release idle outbound connections to reduce demand on I/O resources.
     * All transports are asked to release excess connections.
     */
    public static void shedConnectionCaches() {
        for (TCPTransport transport : allKnownTransports()) {
            transport.shedConnectionCaches();
        }
    }

    /**
     * Export the object to accept incoming calls.
     */
    public void exportObject(Target target) throws RemoteException {
        transport.exportObject(target);
    }

    /**
     * Returns a channel for this (remote) endpoint.
     */
    public Channel getChannel() {
        return getOutboundTransport().getChannel(this);
    }

    /**
     * Returns address for endpoint
     */
    public String getHost() {
        return host;
    }

    /**
     * Returns the port for this endpoint.  If this endpoint was
     * created as a server endpoint (using getLocalEndpoint) for a
     * default/anonymous port and its inbound transport has started
     * listening, this method returns (instead of zero) the actual
     * bound port suitable for passing to clients.
     **/
    public int getPort() {
        return port;
    }

    /**
     * Returns the port that this endpoint's inbound transport listens
     * on, if this endpoint was created as a server endpoint (using
     * getLocalEndpoint).  If this endpoint was created for the
     * default/anonymous port, then this method returns zero even if
     * the transport has started listening.
     **/
    public int getListenPort() {
        return listenPort;
    }

    /**
     * Returns the transport for incoming connections to this
     * endpoint, if this endpoint was created as a server endpoint
     * (using getLocalEndpoint).
     **/
    public Transport getInboundTransport() {
        return transport;
    }

    /**
     * Get the client socket factory associated with this endpoint.
     */
    public RMIClientSocketFactory getClientSocketFactory() {
        return csf;
    }

    /**
     * Get the server socket factory associated with this endpoint.
     */
    public RMIServerSocketFactory getServerSocketFactory() {
        return ssf;
    }

    /**
     * Return string representation for endpoint.
     */
    public String toString() {
        return "[" + host + ":" + port +
            (ssf != null ? "," + ssf : "") +
            (csf != null ? "," + csf : "") +
            "]";
    }

    public int hashCode() {
        return port;
    }

    public boolean equals(Object obj) {
        if ((obj != null) && (obj instanceof TCPEndpoint)) {
            TCPEndpoint ep = (TCPEndpoint) obj;
            if (port != ep.port || !host.equals(ep.host))
                return false;
            if (((csf == null) ^ (ep.csf == null)) ||
                ((ssf == null) ^ (ep.ssf == null)))
                return false;
            /*
             * Fix for 4254510: perform socket factory *class* equality check
             * before socket factory equality check to avoid passing
             * a potentially naughty socket factory to this endpoint's
             * {client,server} socket factory equals method.
             */
            if ((csf != null) &&
                !(csf.getClass() == ep.csf.getClass() && csf.equals(ep.csf)))
                return false;
            if ((ssf != null) &&
                !(ssf.getClass() == ep.ssf.getClass() && ssf.equals(ep.ssf)))
                return false;
            return true;
        } else {
            return false;
        }
    }

    /* codes for the self-describing formats of wire representation */
    private static final int FORMAT_HOST_PORT           = 0;
    private static final int FORMAT_HOST_PORT_FACTORY   = 1;

    /**
     * Write endpoint to output stream.
     */
    public void write(ObjectOutput out) throws IOException {
        if (csf == null) {
            out.writeByte(FORMAT_HOST_PORT);
            out.writeUTF(host);
            out.writeInt(port);
        } else {
            out.writeByte(FORMAT_HOST_PORT_FACTORY);
            out.writeUTF(host);
            out.writeInt(port);
            out.writeObject(csf);
        }
    }

    /**
     * Get the endpoint from the input stream.
     * @param in the input stream
     * @exception IOException If id could not be read (due to stream failure)
     */
    public static TCPEndpoint read(ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        String host;
        int port;
        RMIClientSocketFactory csf = null;

        byte format = in.readByte();
        switch (format) {
          case FORMAT_HOST_PORT:
            host = in.readUTF();
            port = in.readInt();
            break;

          case FORMAT_HOST_PORT_FACTORY:
            host = in.readUTF();
            port = in.readInt();
            csf = (RMIClientSocketFactory) in.readObject();
            if (csf != null && Proxy.isProxyClass(csf.getClass())) {
                throw new IOException("Invalid SocketFactory");
            }
          break;

          default:
            throw new IOException("invalid endpoint format");
        }
        return new TCPEndpoint(host, port, csf, null);
    }

    /**
     * Write endpoint to output stream in older format used by
     * UnicastRef for JDK1.1 compatibility.
     */
    public void writeHostPortFormat(DataOutput out) throws IOException {
        if (csf != null) {
            throw new InternalError("TCPEndpoint.writeHostPortFormat: " +
                "called for endpoint with non-null socket factory");
        }
        out.writeUTF(host);
        out.writeInt(port);
    }

    /**
     * Create a new endpoint from input stream data.
     * @param in the input stream
     */
    public static TCPEndpoint readHostPortFormat(DataInput in)
        throws IOException
    {
        String host = in.readUTF();
        int port = in.readInt();
        return new TCPEndpoint(host, port);
    }

    private static RMISocketFactory chooseFactory() {
        RMISocketFactory sf = RMISocketFactory.getSocketFactory();
        if (sf == null) {
            sf = TCPTransport.defaultSocketFactory;
        }
        return sf;
    }

    /**
     * Open and return new client socket connection to endpoint.
     */
    Socket newSocket() throws RemoteException {
        if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
            TCPTransport.tcpLog.log(Log.VERBOSE,
                "opening socket to " + this);
        }

        Socket socket;

        try {
            RMIClientSocketFactory clientFactory = csf;
            if (clientFactory == null) {
                clientFactory = chooseFactory();
            }
            socket = clientFactory.createSocket(host, port);

        } catch (java.net.UnknownHostException e) {
            throw new java.rmi.UnknownHostException(
                "Unknown host: " + host, e);
        } catch (java.net.ConnectException e) {
            throw new java.rmi.ConnectException(
                "Connection refused to host: " + host, e);
        } catch (IOException e) {
            // We might have simply run out of file descriptors
            try {
                TCPEndpoint.shedConnectionCaches();
                // REMIND: should we retry createSocket?
            } catch (OutOfMemoryError | Exception mem) {
                // don't quit if out of memory
                // or shed fails non-catastrophically
            }

            throw new ConnectIOException("Exception creating connection to: " +
                host, e);
        }

        // set socket to disable Nagle's algorithm (always send immediately)
        // TBD: should this be left up to socket factory instead?
        try {
            socket.setTcpNoDelay(true);
        } catch (Exception e) {
            // if we fail to set this, ignore and proceed anyway
        }

        // fix 4187495: explicitly set SO_KEEPALIVE to prevent client hangs
        try {
            socket.setKeepAlive(true);
        } catch (Exception e) {
            // ignore and proceed
        }

        return socket;
    }

    /**
     * Return new server socket to listen for connections on this endpoint.
     */
    ServerSocket newServerSocket() throws IOException {
        if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
            TCPTransport.tcpLog.log(Log.VERBOSE,
                "creating server socket on " + this);
        }

        RMIServerSocketFactory serverFactory = ssf;
        if (serverFactory == null) {
            serverFactory = chooseFactory();
        }
        ServerSocket server = serverFactory.createServerSocket(listenPort);

        // if we listened on an anonymous port, set the default port
        // (for this socket factory)
        if (listenPort == 0)
            setDefaultPort(server.getLocalPort(), csf, ssf);

        return server;
    }

    /**
     * The class FQDN encapsulates a routine that makes a best effort
     * attempt to retrieve the fully qualified domain name of the local
     * host.
     *
     * @author  Laird Dornin
     */
    private static class FQDN implements Runnable {

        /**
         * strings in which we can store discovered fqdn
         */
        private String reverseLookup;

        private String hostAddress;

        private FQDN(String hostAddress) {
            this.hostAddress = hostAddress;
        }

        /**
         * Do our best to obtain a fully qualified hostname for the local
         * host.  Perform the following steps to get a localhostname:
         *
         * 1. InetAddress.getLocalHost().getHostName() - if contains
         *    '.' use as FQDN
         * 2. if no '.' query name service for FQDN in a thread
         *    Note: We query the name service for an FQDN by creating
         *    an InetAddress via a stringified copy of the local ip
         *    address; this creates an InetAddress with a null hostname.
         *    Asking for the hostname of this InetAddress causes a name
         *    service lookup.
         *
         * 3. if name service takes too long to return, use ip address
         * 4. if name service returns but response contains no '.'
         *    default to ipaddress.
         */
        static String attemptFQDN(InetAddress localAddr)
            throws java.net.UnknownHostException
        {

            String hostName = localAddr.getHostName();

            if (hostName.indexOf('.') < 0 ) {

                String hostAddress = localAddr.getHostAddress();
                FQDN f = new FQDN(hostAddress);

                int nameServiceTimeOut =
                    TCPEndpoint.getInt("sun.rmi.transport.tcp.localHostNameTimeOut",
                                       10000);

                try {
                    synchronized(f) {
                        f.getFQDN();

                        /* wait to obtain an FQDN */
                        f.wait(nameServiceTimeOut);
                    }
                } catch (InterruptedException e) {
                    /* propagate the exception to the caller */
                    Thread.currentThread().interrupt();
                }
                hostName = f.getHost();

                if ((hostName == null) || (hostName.equals(""))
                    || (hostName.indexOf('.') < 0 )) {

                    hostName = hostAddress;
                }
            }
            return hostName;
        }

        /**
         * Method that that will start a thread to wait to retrieve a
         * fully qualified domain name from a name service.  The spawned
         * thread may never return but we have marked it as a daemon so the vm
         * will terminate appropriately.
         */
        private void getFQDN() {

            /* FQDN finder will run in RMI threadgroup. */
            Thread t = AccessController.doPrivileged(
                new NewThreadAction(FQDN.this, "FQDN Finder", true));
            t.start();
        }

        private synchronized String getHost() {
            return reverseLookup;
        }

        /**
         * thread to query a name service for the fqdn of this host.
         */
        public void run()  {

            String name = null;

            try {
                name = InetAddress.getByName(hostAddress).getHostName();
            } catch (java.net.UnknownHostException e) {
            } finally {
                synchronized(this) {
                    reverseLookup = name;
                    this.notify();
                }
            }
        }
    }
}
