/**
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.server;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.ConnectionManager;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.server.RemoteServerConfiguration.Permission;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manages the connection permissions for remote servers. When a remote server is allowed to
 * connect to this server then a special configuration for the remote server will be kept.
 * The configuration holds information such as the port to use when creating an outgoing connection.
 *
 * @author Gaston Dombiak
 */
public class RemoteServerManager {

	private static final Logger Log = LoggerFactory.getLogger(RemoteServerManager.class);

    private static final String ADD_CONFIGURATION =
        "INSERT INTO ofRemoteServerConf (xmppDomain,remotePort,permission) VALUES (?,?,?)";
    private static final String DELETE_CONFIGURATION =
        "DELETE FROM ofRemoteServerConf WHERE xmppDomain=?";
    private static final String LOAD_CONFIGURATION =
        "SELECT remotePort,permission FROM ofRemoteServerConf where xmppDomain=?";
    private static final String LOAD_CONFIGURATIONS =
        "SELECT xmppDomain,remotePort FROM ofRemoteServerConf where permission=?";

    private static Cache configurationsCache;

    static {
        configurationsCache = CacheFactory.createCache("Remote Server Configurations");
    }

    /**
     * Allows a remote server to connect to the local server with the specified configuration.
     *
     * @param configuration the configuration for the remote server.
     */
    public static void allowAccess(RemoteServerConfiguration configuration) {
        // Remove any previous configuration for this remote server
        deleteConfiguration(configuration.getDomain());
        // Update the database with the new granted permission and configuration
        configuration.setPermission(Permission.allowed);
        addConfiguration(configuration);
    }

    /**
     * Blocks a remote server from connecting to the local server. If the remote server was
     * connected when the permission was revoked then the connection of the entity will be closed.
     *
     * @param domain the domain of the remote server that is not allowed to connect.
     */
    public static void blockAccess(String domain) {
        // Remove any previous configuration for this remote server
        deleteConfiguration(domain);
        // Update the database with the new revoked permission
        RemoteServerConfiguration config = new RemoteServerConfiguration(domain);
        config.setPermission(Permission.blocked);
        addConfiguration(config);
        // Check if the remote server was connected and proceed to close the connection
        for (Session session : SessionManager.getInstance().getIncomingServerSessions(domain)) {
            session.close();
        }
        Session session = SessionManager.getInstance().getOutgoingServerSession(domain);
        if (session != null) {
            session.close();
        }
    }

    /**
     * Returns true if the remote server with the specified domain can connect to the
     * local server.
     *
     * @param domain the domain of the remote server.
     * @return true if the remote server with the specified domain can connect to the
     *         local server.
     */
    public static boolean canAccess(String domain) {
        // If s2s is disabled then it is not possible to send packets to remote servers or
        // receive packets from remote servers
        if (!JiveGlobals.getBooleanProperty(ConnectionSettings.Server.SOCKET_ACTIVE, true)) {
            return false;
        }

        // By default there is no permission defined for the XMPP entity
        Permission permission = null;

        RemoteServerConfiguration config = getConfiguration(domain);
        if (config != null) {
            permission = config.getPermission();
        }

        if (PermissionPolicy.blacklist == getPermissionPolicy()) {
            // Anyone can access except those entities listed in the blacklist
            return Permission.blocked != permission;
        }
        else {
            // Access is limited to those present in the whitelist
            return Permission.allowed == permission;
        }
    }

    /**
     * Returns the list of registered remote servers that are allowed to connect to/from this
     * server when using a whitelist policy. However, when using a blacklist policy (i.e. anyone
     * may connect to the server) the returned list of configurations will be used for obtaining
     * the specific connection configuration for each remote server.
     *
     * @return the configuration of the registered external components.
     */
    public static Collection<RemoteServerConfiguration> getAllowedServers() {
        return getConfigurations(Permission.allowed);
    }

    /**
     * Returns the list of remote servers that are NOT allowed to connect to/from this
     * server.
     *
     * @return the configuration of the blocked external components.
     */
    public static Collection<RemoteServerConfiguration> getBlockedServers() {
        return getConfigurations(Permission.blocked);
    }

    /**
     * Returns the number of milliseconds to wait to connect to a remote server or read
     * data from a remote server. Default timeout value is 20 seconds. Configure the
     * <tt>xmpp.server.read.timeout</tt> global property to override the default value.
     *
     * @return the number of milliseconds to wait to connect to a remote server or read
     *         data from a remote server.
     */
    public static int getSocketTimeout() {
        return JiveGlobals.getIntProperty(ConnectionSettings.Server.SOCKET_READ_TIMEOUT, 120000);
    }

    /**
     * Removes any existing defined permission and configuration for the specified
     * remote server.
     *
     * @param domain the domain of the remote server.
     */
    public static void deleteConfiguration(String domain) {
        // Remove configuration from cache
        configurationsCache.remove(domain);
        // Remove the permission for the entity from the database
        java.sql.Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_CONFIGURATION);
            pstmt.setString(1, domain);
            pstmt.executeUpdate();
        }
        catch (SQLException sqle) {
            Log.error(sqle.getMessage(), sqle);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    /**
     * Adds a new permission for the specified remote server.
     *
     * @param configuration the new configuration for a remote server
     */
    private static void addConfiguration(RemoteServerConfiguration configuration) {
        // Remove configuration from cache
        configurationsCache.put(configuration.getDomain(), configuration);
        // Remove the permission for the entity from the database
        java.sql.Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(ADD_CONFIGURATION);
            pstmt.setString(1, configuration.getDomain());
            pstmt.setInt(2, configuration.getRemotePort());
            pstmt.setString(3, configuration.getPermission().toString());
            pstmt.executeUpdate();
        }
        catch (SQLException sqle) {
            Log.error(sqle.getMessage(), sqle);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    /**
     * Returns the configuration for a remote server or <tt>null</tt> if none was found.
     *
     * @param domain the domain of the remote server.
     * @return the configuration for a remote server or <tt>null</tt> if none was found.
     */
    public static RemoteServerConfiguration getConfiguration(String domain) {
        Object value = configurationsCache.get(domain);
        if ("null".equals(value)) {
            return null;
        }
        RemoteServerConfiguration configuration = (RemoteServerConfiguration) value;
        if (configuration == null) {
            java.sql.Connection con = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                con = DbConnectionManager.getConnection();
                pstmt = con.prepareStatement(LOAD_CONFIGURATION);
                pstmt.setString(1, domain);
                rs = pstmt.executeQuery();
                while (rs.next()) {
                    configuration = new RemoteServerConfiguration(domain);
                    configuration.setRemotePort(rs.getInt(1));
                    configuration.setPermission(Permission.valueOf(rs.getString(2)));
                }
            }
            catch (SQLException sqle) {
                Log.error(sqle.getMessage(), sqle);
            }
            finally {
                DbConnectionManager.closeConnection(rs, pstmt, con);
            }
            if (configuration != null) {
                configurationsCache.put(domain, configuration);
            }
            else {
                configurationsCache.put(domain, "null");
            }
        }
        return configuration;
    }

    private static Collection<RemoteServerConfiguration> getConfigurations(
            Permission permission) {
        Collection<RemoteServerConfiguration> answer =
                new ArrayList<>();
        java.sql.Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_CONFIGURATIONS);
            pstmt.setString(1, permission.toString());
            rs = pstmt.executeQuery();
            RemoteServerConfiguration configuration;
            while (rs.next()) {
                configuration = new RemoteServerConfiguration(rs.getString(1));
                configuration.setRemotePort(rs.getInt(2));
                configuration.setPermission(permission);
                answer.add(configuration);
            }
        }
        catch (SQLException sqle) {
            Log.error(sqle.getMessage(), sqle);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return answer;
    }

    /**
     * Returns the remote port to connect for the specified remote server. If no port was
     * defined then use the default port (e.g. 5269).
     *
     * @param domain the domain of the remote server to get the remote port to connect to.
     * @return the remote port to connect for the specified remote server.
     */
    public static int getPortForServer(String domain) {
        int port = JiveGlobals.getIntProperty(ConnectionSettings.Server.REMOTE_SERVER_PORT, ConnectionManager.DEFAULT_SERVER_PORT);
        RemoteServerConfiguration config = getConfiguration(domain);
        if (config != null) {
            port = config.getRemotePort();
            if (port == 0) {
                port = JiveGlobals
                        .getIntProperty(ConnectionSettings.Server.REMOTE_SERVER_PORT, ConnectionManager.DEFAULT_SERVER_PORT);
            }
        }
        return port;
    }

    /**
     * Returns the permission policy being used for new XMPP entities that are trying to
     * connect to the server. There are two types of policies: 1) blacklist: where any entity
     * is allowed to connect to the server except for those listed in the black list and
     * 2) whitelist: where only the entities listed in the white list are allowed to connect to
     * the server.
     *
     * @return the permission policy being used for new XMPP entities that are trying to
     *         connect to the server.
     */
    public static PermissionPolicy getPermissionPolicy() {
        try {
            return PermissionPolicy.valueOf(JiveGlobals.getProperty(ConnectionSettings.Server.PERMISSION_SETTINGS,
                    PermissionPolicy.blacklist.toString()));
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
            return PermissionPolicy.blacklist;
        }
    }

    /**
     * Sets the permission policy being used for new XMPP entities that are trying to
     * connect to the server. There are two types of policies: 1) blacklist: where any entity
     * is allowed to connect to the server except for those listed in the black list and
     * 2) whitelist: where only the entities listed in the white list are allowed to connect to
     * the server.
     *
     * @param policy the new PermissionPolicy to use.
     */
    public static void setPermissionPolicy(PermissionPolicy policy) {
        JiveGlobals.setProperty(ConnectionSettings.Server.PERMISSION_SETTINGS, policy.toString());
        // Check if the connected servers can remain connected to the server
        for (String hostname : SessionManager.getInstance().getIncomingServers()) {
            if (!canAccess(hostname)) {
                for (Session session : SessionManager.getInstance().getIncomingServerSessions(hostname)) {
                    session.close();
                }
            }
        }
        for (String hostname : SessionManager.getInstance().getOutgoingServers()) {
            if (!canAccess(hostname)) {
                Session session = SessionManager.getInstance().getOutgoingServerSession(hostname);
                session.close();
            }
        }
    }

    /**
     * Sets the permission policy being used for new XMPP entities that are trying to
     * connect to the server. There are two types of policies: 1) blacklist: where any entity
     * is allowed to connect to the server except for those listed in the black list and
     * 2) whitelist: where only the entities listed in the white list are allowed to connect to
     * the server.
     *
     * @param policy the new policy to use.
     */
    public static void setPermissionPolicy(String policy) {
        setPermissionPolicy(PermissionPolicy.valueOf(policy));
    }

    public enum PermissionPolicy {
        /**
         * Any XMPP entity is allowed to connect to the server except for those listed in
         * the <b>not allowed list</b>.
         */
        blacklist,

        /**
         * Only the XMPP entities listed in the <b>allowed list</b> are able to connect to
         * the server.
         */
        whitelist;
    }
}
