/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.qpid.server.security.access.plugins.network;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.qpid.server.protocol.AMQMinaProtocolSession;
import org.apache.qpid.server.protocol.AMQProtocolSession;
import org.apache.qpid.server.security.access.ACLPlugin;
import org.apache.qpid.server.security.access.ACLPluginFactory;
import org.apache.qpid.server.security.access.plugins.AbstractACLPlugin;
import org.apache.qpid.server.virtualhost.VirtualHost;
import org.apache.qpid.util.NetMatcher;

public class FirewallPlugin extends AbstractACLPlugin
{

    public class FirewallPluginException extends Exception {}

    public static final ACLPluginFactory FACTORY = new ACLPluginFactory()
    {
        public boolean supportsTag(String name)
        {
            return name.startsWith("firewall");
        }

        public ACLPlugin newInstance(Configuration config) throws ConfigurationException
        {
            FirewallPlugin plugin = new FirewallPlugin();
            plugin.setConfiguration(config.subset("firewall"));
            return plugin;
        }
    };
    
    public class FirewallRule
    {

        private static final long DNS_TIMEOUT = 30000;
        private AuthzResult _access;
        private NetMatcher _network;
        private Pattern[] _hostnamePatterns;

        public FirewallRule(String access, List networks, List hostnames)
        {
            _access = (access.equals("allow")) ? AuthzResult.ALLOWED : AuthzResult.DENIED;
            
            if (networks != null && networks.size() > 0)
            {
                String[] networkStrings = objListToStringArray(networks);
                _network = new NetMatcher(networkStrings);
            }
            
            if (hostnames != null && hostnames.size() > 0)
            {
                int i = 0;
                _hostnamePatterns = new Pattern[hostnames.size()];
                for (String hostname : objListToStringArray(hostnames))
                {
                    _hostnamePatterns[i++] = Pattern.compile(hostname);
                }
            }
            
        }

        private String[] objListToStringArray(List objList)
        {
            String[] networkStrings = new String[objList.size()];
            int i = 0;
            for (Object network : objList)
            {
                networkStrings[i++] = (String) network;
            }
            return networkStrings;
        }

        public boolean match(InetAddress remote) throws FirewallPluginException
        {
            if (_hostnamePatterns != null)
            {
                String hostname = getHostname(remote);
                if (hostname == null)
                {
                    throw new FirewallPluginException();
                }
                for (Pattern pattern : _hostnamePatterns)
                {
                    if (pattern.matcher(hostname).matches())
                    {
                        return true;
                    }
                }
                return false;
            }
            else
            {
                return _network.matchInetNetwork(remote);
            }
        }

        /**
         * @param remote the InetAddress to look up
         * @return the hostname, null if not found or takes longer than 30s to find
         */
        private String getHostname(final InetAddress remote)
        {
            final String[] hostname = new String[]{null};
            final AtomicBoolean done = new AtomicBoolean(false);
            // Spawn thread
            Thread thread = new Thread(new Runnable()
            {
               public void run()
               {
                   hostname[0] = remote.getCanonicalHostName();
                   done.getAndSet(true);
                   synchronized (done)
                   {
                       done.notifyAll();
                   }
               }
            });

            thread.run();
            long endTime = System.currentTimeMillis() + DNS_TIMEOUT;
            
            while (System.currentTimeMillis() < endTime && !done.get())
            {
                try
                {
                    synchronized (done)
                    {
                        done.wait(endTime - System.currentTimeMillis());
                    }
                }
                catch (InterruptedException e)
                {
                    // Check the time and if necessary sleep for a bit longer
                }
            }
            return hostname[0];
        }

        public AuthzResult getAccess()
        {
            return _access;
        }

    }

    private AuthzResult _default = AuthzResult.ABSTAIN;
    private FirewallRule[] _rules;

    @Override
    public AuthzResult authoriseConnect(AMQProtocolSession session, VirtualHost virtualHost)
    {
        if (!(session instanceof AMQMinaProtocolSession))
        {
            return AuthzResult.ABSTAIN; // We only deal with tcp sessions, which
                                        // mean MINA right now
        }

        InetAddress addr = getInetAdressFromMinaSession((AMQMinaProtocolSession) session);

        if (addr == null)
        {
            return AuthzResult.ABSTAIN; // Not an Inet socket on the other end
        }

        boolean match = false;
        for (FirewallRule rule : _rules)
        {
            try
            {
                match = rule.match(addr);
            }
            catch (FirewallPluginException e)
            {
                return AuthzResult.DENIED;
            }
            if (match)
            {
                return rule.getAccess();
            }
        }
        return _default;

    }

    private InetAddress getInetAdressFromMinaSession(AMQMinaProtocolSession session)
    {
        SocketAddress remote = session.getIOSession().getRemoteAddress();
        if (remote instanceof InetSocketAddress)
        {
            return ((InetSocketAddress) remote).getAddress();
        }
        else
        {
            return null;
        }
    }

    @Override
    public void setConfiguration(Configuration config) throws ConfigurationException
    {
        // Get default action
        String defaultAction = config.getString("[@default-action]");
        if (defaultAction == null)
        {
            _default = AuthzResult.ABSTAIN;
        }
        else if (defaultAction.toLowerCase().equals("allow"))
        {
            _default = AuthzResult.ALLOWED;
        }
        else
        {
            _default = AuthzResult.DENIED;
        }
        CompositeConfiguration finalConfig = new CompositeConfiguration(config);
        
        List subFiles = config.getList("xml[@fileName]");
        for (Object subFile : subFiles)
        {
            finalConfig.addConfiguration(new XMLConfiguration((String) subFile));
        }

        // all rules must have an access attribute
        int numRules = finalConfig.getList("rule[@access]").size(); 
        _rules = new FirewallRule[numRules];
        for (int i = 0; i < numRules; i++)
        {
            FirewallRule rule = new FirewallRule(finalConfig.getString("rule(" + i + ")[@access]"), finalConfig.getList("rule("
                    + i + ")[@network]"), finalConfig.getList("rule(" + i + ")[@hostname]"));
            _rules[i] = rule;
        }
    }
}
