/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: PhexSecurityManager.java,v 1.15 2004/08/26 22:18:28 gregork Exp $
 */
package phex.security;

import java.io.*;
import java.net.*;
import java.util.*;
import javax.xml.bind.*;

import phex.common.*;
import phex.event.*;
import phex.host.*;
import phex.utils.*;
import phex.xml.*;
import phex.xml.ObjectFactory;

// TODO update list from http://methlabs.org/sync/
public class PhexSecurityManager implements Manager
{
    public static final byte ACCESS_GRANTED = 0x01;
    public static final byte ACCESS_DENIED = 0x02;
    public static final byte ACCESS_STRONGLY_DENIED = 0x03;

    private static PhexSecurityManager instance;

    private ArrayList ipAccessRuleList;

    public PhexSecurityManager()
    {
        ipAccessRuleList = new ArrayList();
    }

    public int getIPAccessRuleCount()
    {
        synchronized( ipAccessRuleList )
        {
            return ipAccessRuleList.size();
        }
    }

    public IPAccessRule getIPAccessRule( int index )
    {
        synchronized( ipAccessRuleList )
        {
            if ( index < 0 || index >= ipAccessRuleList.size() )
            {
                return null;
            }
            return (IPAccessRule) ipAccessRuleList.get( index );
        }
    }

    public IPAccessRule[] getIPAccessRulesAt( int[] indices )
    {
        synchronized( ipAccessRuleList )
        {
            int length = indices.length;
            IPAccessRule[] rules = new IPAccessRule[ length ];
            int listSize = ipAccessRuleList.size();
            for ( int i = 0; i < length; i++ )
            {
                if ( indices[i] < 0 || indices[i] >= listSize )
                {
                    rules[i] = null;
                }
                else
                {
                    rules[i] = (IPAccessRule)ipAccessRuleList.get( indices[i] );
                }
            }
            return rules;
        }
    }


    public IPAccessRule createIPAccessRule( String description,
        boolean isDenyingRule, byte type, byte[] ip, byte[] compareIP,
        boolean isDisabled, ExpiryDate expiryDate, boolean isDeletedOnExpiry )
    {
        IPAccessRule rule = new IPAccessRule( description, isDenyingRule, type,
            ip, compareIP );

        rule.setDisabled( isDisabled );
        rule.setExpiryDate( expiryDate );
        rule.setDeleteOnExpiry( isDeletedOnExpiry );

        synchronized( ipAccessRuleList )
        {
            int position = ipAccessRuleList.size();
            ipAccessRuleList.add( position, rule );
            fireSecurityRuleAdded( position );
        }
        return rule;
    }

    public void removeSecurityRule( SecurityRule rule )
    {
        synchronized( ipAccessRuleList )
        {
            int idx = ipAccessRuleList.indexOf( rule );
            if ( idx != -1 )
            {
                ipAccessRuleList.remove( idx );
                fireSecurityRuleRemoved( idx );
            }
        }
    }

    public byte controlHostAddressAccess( HostAddress address )
        throws UnknownHostException
    {
        byte[] hostIP = address.getHostIP();
        return controlHostIPAccess( hostIP );
    }

    public byte controlHostIPAccess( byte[] hostIP )
    {
        synchronized( ipAccessRuleList )
        {
            Iterator iterator = ipAccessRuleList.iterator();
            IPAccessRule rule;
            while( iterator.hasNext() )
            {
                rule = (IPAccessRule) iterator.next();
                if ( rule.isDisabled() )
                {// skip disabled rules...
                    continue;
                }
                if ( !rule.isHostIPAllowed( hostIP ) )
                {
                    if ( rule.isStrongFilter() )
                    {
                        return ACCESS_STRONGLY_DENIED;
                    }
                    return ACCESS_DENIED;
                }
            }
        }
        return ACCESS_GRANTED;
    }

    private void loadHostileHostList( XJBSecurity xjbSecurity )
    {
        try
        {
            Logger.logMessage( Logger.FINE, Logger.NETWORK,
                "Load hostile hosts file." );
            InputStream inStream = ClassLoader.getSystemResourceAsStream(
                "phex/resources/hostilehosts.cfg" );
            BufferedReader br;
            if ( inStream != null )
            {
                br = new BufferedReader( new InputStreamReader( inStream ) );
            }
            else
            {
                Logger.logMessage( Logger.FINE, Logger.NETWORK,
                    "Hostile hosts file not found." );
                return;
            }

            String line;
            IPAccessRule rule;
            while ( (line = br.readLine()) != null)
            {
                if ( line.startsWith("#") )
                {
                    continue;
                }
                int slashIdx = line.indexOf( '/' );
                byte[] ip;
                byte[] mask;
                byte type;
                if ( slashIdx == -1 )
                {// single ip...
                    ip = HostAddress.parseIP( line );
                    mask = null;
                    type = IPAccessRule.SINGLE_ADDRESS;
                }
                else
                {
                    String ipStr = line.substring( 0, slashIdx ).trim();
                    String maskStr = line.substring( slashIdx + 1 ).trim();
                    ip = HostAddress.parseIP( ipStr );
                    mask = HostAddress.parseIP( maskStr );
                    type = IPAccessRule.NETWORK_MASK;
                }
                rule = new IPAccessRule( "System rule.", true, type, ip, mask );
                rule.setSystemRule( true );
                rule.setStrongFilter( true );
                rule.setDisabled( false );
                // adjust hit count..
                XJBSecurityRule xjbRule = findSystemXJBRule( xjbSecurity, ip,
                    mask );
                if ( xjbRule != null )
                {
                    rule.setTriggerCount( xjbRule.getTriggerCount() );
                }
                ipAccessRuleList.add( rule );
            }
            br.close();
        }
        catch ( IOException exp )
        {
            Logger.logWarning( exp );
        }
    }

    private XJBSecurityRule findSystemXJBRule( XJBSecurity xjbSecurity,
        byte[] ip, byte[] mask )
    {
        List ruleList = xjbSecurity.getIpAccessRuleList();
        Iterator iterator = ruleList.iterator();
        XJBIPAccessRule xjbRule;
        while ( iterator.hasNext() )
        {
            xjbRule = ( XJBIPAccessRule )iterator.next();
            if ( !xjbRule.isSystemRule() )
            {
                continue;
            }
            if ( Arrays.equals( xjbRule.getIp(), ip ) )
            {
                if ( mask != null && Arrays.equals( xjbRule.getCompareIP(), mask ) )
                {
                    return xjbRule;
                }
            }
        }
        return null;
    }

    private void loadSecurityRuleList()
    {
        Logger.logMessage( Logger.CONFIG, Logger.GLOBAL,
            "Loading security rule list..." );
        File securityFile = Environment.getInstance().getPhexConfigFile(
            EnvironmentConstants.XML_SECURITY_FILE_NAME );
        XJBPhex phex;
        try
        {
            phex = XMLBuilder.loadXJBPhexFromFile( securityFile );
            if ( phex == null )
            {
                Logger.logMessage( Logger.SEVERE, Logger.GLOBAL,
                    "Error loading security rule list." );
                phex = ObjectFactory.createXJBPhex();
            }
            XJBSecurity xjbSecurity = phex.getSecurity();
            if ( xjbSecurity == null )
            {
                Logger.logMessage( Logger.FINE, Logger.GLOBAL,
                    "No security definition found." );
                xjbSecurity = ObjectFactory.createXJBSecurity();
            }
            List ruleList = xjbSecurity.getIpAccessRuleList();
            synchronized( ipAccessRuleList )
            {
                Iterator iterator = ruleList.iterator();
                XJBIPAccessRule xjbRule;
                IPAccessRule rule;
                while( iterator.hasNext() )
                {
                    xjbRule = (XJBIPAccessRule)iterator.next();
                    if ( !xjbRule.isSystemRule() )
                    {
                        rule = new IPAccessRule( xjbRule );
                        ipAccessRuleList.add( rule );
                    }
                }
                loadHostileHostList( xjbSecurity );
            }
        }
        catch ( JAXBException exp )
        {
            // TODO bring a GUI message that file cant be created
            Logger.logError( exp );
            return;
        }

    }

    private void saveSecurityRuleList()
    {
        Logger.logMessage( Logger.CONFIG, Logger.GLOBAL,
            "Saving security rule list..." );

        try
        {
            XJBPhex phex = ObjectFactory.createPhexElement();

            XJBSecurity security = ObjectFactory.createXJBSecurity();
            phex.setSecurity( security );
            phex.setPhexVersion( VersionUtils.getProgramVersion() );

            List ruleList = security.getIpAccessRuleList();
            synchronized( ipAccessRuleList )
            {
                Iterator iterator = ipAccessRuleList.iterator();
                IPAccessRule rule;
                while ( iterator.hasNext() )
                {
                    rule = ( IPAccessRule )iterator.next();
                    if ( !rule.isSystemRule() && rule.isDeletedOnExpiry() && 
                         ( rule.getExpiryDate().isExpiringEndOfSession() ||
                           rule.getExpiryDate().isExpired() ) )
                    {// skip session expiry rules that get deleted on expiry...
                     // except if they are system rules
                        continue;
                    }

                    XJBSecurityRule xjbRule = rule.createXJBSecurityRule();
                    ruleList.add( xjbRule );
                }
            }

            File securityFile = Environment.getInstance().getPhexConfigFile(
                EnvironmentConstants.XML_SECURITY_FILE_NAME );

            XMLBuilder.saveToFile( securityFile, phex );
        }
        catch ( JAXBException exp )
        {
            // TODO bring a GUI message that file cant be created
            Logger.logError( exp );
        }
    }

    private void updateOldIpFilterIterator( Iterator iterator, String text )
    {
        byte[] fromIP;
        byte[] toIP;
        while ( iterator.hasNext() )
        {
            boolean isRange = false;
            String[] ipFilter = (String[])iterator.next();
            fromIP = new byte[4];
            toIP = null;

            for ( int i = 0; i < 4; i++ )
            {
                if ( ipFilter[i].equals("*"))
                {
                    if ( !isRange )
                    {
                        toIP = new byte[4];
                        for ( int j=0; j < i; j++ )
                        {
                            toIP[j] = fromIP[j];
                        }
                        isRange = true;
                    }
                    fromIP[i] = 0x00;
                    toIP[i] = (byte)0xff;
                }
                else
                {
                    fromIP[i] = (byte)Integer.parseInt( ipFilter[i] );
                    if ( isRange )
                    {
                        toIP[i] = fromIP[i];
                    }
                }
            }
            byte type;
            if ( isRange )
            {
                type = IPAccessRule.NETWORK_RANGE;
            }
            else
            {
                type = IPAccessRule.SINGLE_ADDRESS;
            }
            String description = Localizer.getFormatedString( "ConvertedRule",
               new String[]{ text } );
            IPAccessRule rule = new IPAccessRule(
                description, true, type, fromIP, toIP );
            synchronized( ipAccessRuleList )
            {
                ipAccessRuleList.add( rule );
            }
        }
    }

    //////////////////////// Start Manager interface ///////////////////////////

    public static PhexSecurityManager getInstance()
    {
        if ( instance == null )
        {
            instance = new PhexSecurityManager();
        }
        return instance;
    }

    /**
     * This method is called in order to initialize the manager. This method
     * includes all tasks that must be done to intialize all the several manager.
     * Like instantiating the singleton instance of the manager. Inside
     * this method you can't rely on the availability of other managers.
     * @return true is initialization was successful, false otherwise.
     */
    public boolean initialize()
    {
        return true;
    }

    /**
     * This method is called in order to perform post initialization of the
     * manager. This method includes all tasks that must be done after initializing
     * all the several managers. Inside this method you can rely on the
     * availability of other managers.
     * @return true is initialization was successful, false otherwise.
     */
    public boolean onPostInitialization()
    {
        loadSecurityRuleList();
        return true;
    }
    
    /**
     * This method is called after the complete application including GUI completed
     * its startup process. This notification must be used to activate runtime
     * processes that needs to be performed once the application has successfully
     * completed startup.
     */
    public void startupCompletedNotify()
    {
    }

    /**
     * This method is called in order to cleanly shutdown the manager. It
     * should contain all cleanup operations to ensure a nice shutdown of Phex.
     */
    public void shutdown()
    {
        saveSecurityRuleList();
    }
    //////////////////////// End Manager interface ///////////////////////////

    ///////////////////// START event handling methods /////////////////////////

    /**
     * All listeners interested in events.
     */
    private ArrayList listenerList = new ArrayList( 2 );

    public void addUploadFilesChangeListener( SecurityRulesChangeListener listener )
    {
        listenerList.add( listener );
    }

    public void removeUploadFilesChangeListener( SecurityRulesChangeListener listener )
    {
        listenerList.remove( listener );
    }

    private void fireSecurityRuleChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SecurityRulesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SecurityRulesChangeListener)listeners[ i ];
                    listener.securityRuleChanged( position );
                }
            }
        });
    }

    private void fireSecurityRuleAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SecurityRulesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SecurityRulesChangeListener)listeners[ i ];
                    listener.securityRuleAdded( position );
                }
            }
        });
    }

    private void fireSecurityRuleRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                SecurityRulesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (SecurityRulesChangeListener)listeners[ i ];
                    listener.securityRuleRemoved( position );
                }
            }
        });
    }

    public void fireSecurityRuleChanged( SecurityRule rule )
    {
        synchronized( ipAccessRuleList )
        {
            int position = ipAccessRuleList.indexOf( rule );
            if ( position >= 0 )
            {
                fireSecurityRuleChanged( position );
            }
        }
    }
    ///////////////////// END event handling methods ////////////////////////
}