/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2007 Phex Development Group
 *
 *  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
 * 
 *  --- SVN Information ---
 *  $Id: PhexSecurityManager.java 3867 2007-07-06 16:26:27Z gregork $
 */
package phex.security;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import phex.common.AbstractManager;
import phex.common.Environment;
import phex.common.EnvironmentConstants;
import phex.common.ExpiryDate;
import phex.common.ThreadPool;
import phex.common.URN;
import phex.common.address.AddressUtils;
import phex.common.address.DestAddress;
import phex.common.address.IpAddress;
import phex.common.file.FileManager;
import phex.common.file.ManagedFile;
import phex.common.file.ManagedFileException;
import phex.event.SecurityRulesChangeListener;
import phex.event.UserMessageListener;
import phex.prefs.core.SecurityPrefs;
import phex.utils.NLogger;
import phex.utils.NLoggerNames;
import phex.utils.StringUtils;
import phex.utils.VersionUtils;
import phex.xml.sax.DPhex;
import phex.xml.sax.XMLBuilder;
import phex.xml.sax.security.DIpAccessRule;
import phex.xml.sax.security.DSecurity;
import phex.xml.sax.security.DSecurityRule;

public class PhexSecurityManager extends AbstractManager
{
    private static final String[] SHA1_FILES =
    {
        "BearShare and LimeWire Pro scams and worms.SHA1",
        "Fakes.SHA1",
        "SPAM - 0-8K bytes - How2 [open with notepad.avi].SHA1",
        "SPAM - 15,872, 70-170K bytes - EFreeClub.SHA1",
        "SPAM - 22 bytes - Empty zip files.SHA1",
        "SPAM - Various.SHA1",
        "TROJAN - 50-80K bytes - ISTbar.SHA1",
        "TROJAN - 61-62K bytes - Mainpean StarDialer.SHA1",
        "TROJAN - 81,964 bytes - WinVBIE Toolbar.SHA1",
        "TROJAN - 233472 bytes - Dropper.Generic.DZD.SHA1",
        "TROJAN - 783843 bytes - Crypt.B.SHA1",
        "WORM - 71,070 bytes - W32.Alcra.C.SHA1",
        "WORM - 123897 bytes - GEDZAC VBS-Israfel.SHA1",
        "WORM - 178861 bytes - IRC.Backdoor.SdBot.LFI.SHA1",
        "WORM - 202477 bytes - Generic.FX!CME-24.SHA1",
        "WORM - 535082 bytes - W32.Alcra.D.SHA1",
        "WORM - 643767 bytes - VB.FL.SHA1",
        "WORM - 872159 bytes - VB.CC.SHA1",
        "Phex Collected.SHA1"
    };
    
    /**
     * A list of all ip access rules.
     */
    private ArrayList<IpSecurityRule> ipAccessRuleList;
    
    /**
     * Ip list containing Phex default system rules. 
     */
    private final IpSystemRuleList ipSystemRuleList;
    
    /**
     * Ip list containing user rules. We mainly need an additional list for users
     * to distinguish between different AccessTypes.
     */
    private final IpSystemRuleList ipUserRuleList;
    
    private final Set<String> blockedUrnSet;
    

    public PhexSecurityManager()
    {
        ipAccessRuleList = new ArrayList<IpSecurityRule>();
        ipSystemRuleList = new IpSystemRuleList();
        ipUserRuleList = new IpSystemRuleList();
        blockedUrnSet = new HashSet<String>();
    }
    
    public AccessType controlUrnAccess( URN urn )
    {
        if ( urn.isSha1Nid() )
        {
            if( blockedUrnSet.contains( urn.getSHA1Nss() ) )
            {
                return AccessType.ACCESS_STRONGLY_DENIED;
            }
        }
        return AccessType.ACCESS_GRANTED;
    }
    
    private void loadHostileSha1List()
    {
        if ( !SecurityPrefs.LoadHostileSha1List.get().booleanValue() )
        {
            return;
        }
        try
        {
            NLogger.debug( NLoggerNames.Security,
                "Load hostile sha1 files." );
            long start = System.currentTimeMillis();
            for ( int i = 0; i < SHA1_FILES.length; i++ )
            {
                InputStream inStream = ClassLoader.getSystemResourceAsStream(
                    "phex/resources/sha1/" + SHA1_FILES[i]  );
                BufferedReader br;
                if ( inStream != null )
                {
                    br = new BufferedReader( new InputStreamReader( inStream ) );
                }
                else
                {
                    NLogger.debug( NLoggerNames.Security,
                        "Hostile sha1 file not found: " + SHA1_FILES[i] );
                    continue;
                }

                String line;
                while ( (line = br.readLine()) != null)
                {
                    if ( StringUtils.isEmpty(line) || line.startsWith("#") )
                    {
                        continue;
                    }
                    if ( line.length() != 32 )
                    {
                        NLogger.warn( NLoggerNames.Security,
                            "Skip invalid line: " + line + " in " + SHA1_FILES[i]);
                        continue;
                    }
                    boolean succ = blockedUrnSet.add(line);
                    if ( !succ && NLogger.isDebugEnabled( NLoggerNames.Security ) )
                    {
                        NLogger.warn( NLoggerNames.Security,
                            "Found dupplicate: " + line + " in " + SHA1_FILES[i] );
                    }
                }
                br.close();
            }
            long end = System.currentTimeMillis();
            NLogger.debug( NLoggerNames.Security,
                "Loaded hostile sha1 file: " + (end-start) );
        }
        catch ( IOException exp )
        {
            NLogger.warn( NLoggerNames.Security, exp, exp );
        }
    }
    
    
    /////////////////// IP access rules //////////////////////

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

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

    public IpSecurityRule[] getIPAccessRulesAt( int[] indices )
    {
        synchronized( ipAccessRuleList )
        {
            int length = indices.length;
            IpSecurityRule[] rules = new IpSecurityRule[ 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] = ipAccessRuleList.get( indices[i] );
                }
            }
            return rules;
        }
    }

    public IpUserSecurityRule createIPAccessRule( String description,
        byte[] ip, byte cidr, boolean isDisabled, ExpiryDate expiryDate, 
        boolean isDeletedOnExpiry )
    {
        IpUserSecurityRule rule = new IpUserSecurityRule( description, 
            ip, cidr, isDisabled, isDeletedOnExpiry, expiryDate );

        int position;
        synchronized( ipAccessRuleList )
        {
            position = ipAccessRuleList.size();
            ipAccessRuleList.add( rule );
        }
        ipUserRuleList.add( rule );
        fireSecurityRuleAdded( rule, position );
        return rule;
    }
    
    public IpUserSecurityRule updateIpUserSecurityRule( IpUserSecurityRule oldRule,
        String description, byte[] ip, byte cidr, boolean isDisabled, 
        ExpiryDate expiryDate, boolean isDeletedOnExpiry )
    {
        IpUserSecurityRule rule = new IpUserSecurityRule( description, 
            ip, cidr, isDisabled, isDeletedOnExpiry, expiryDate );
        rule.setTriggerCount( oldRule.getTriggerCount() );
        
        removeSecurityRule( oldRule );
        
        int position;
        synchronized( ipAccessRuleList )
        {
            position = ipAccessRuleList.size();
            ipAccessRuleList.add( rule );
        }
        ipUserRuleList.add( rule );
        fireSecurityRuleAdded( rule, position );
        return rule;
    }

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

    public AccessType controlHostAddressAccess( DestAddress address )
    {
        IpAddress ipAddress = address.getIpAddress();
        if ( ipAddress == null )
        {// no ip address... security is not checking not ip based  addresses.
            return AccessType.ACCESS_GRANTED;
        }
        byte[] hostIP = ipAddress.getHostIP();
        return controlHostIPAccess( hostIP );
    }

    public AccessType controlHostIPAccess( byte[] hostIP )
    {
        IpCidrPair checkPair = new IpCidrPair( AddressUtils.byteIpToIntIp(hostIP) );
        boolean contains = ipSystemRuleList.contains( checkPair );
        if ( contains )
        {
            return AccessType.ACCESS_STRONGLY_DENIED;
        }
        
        contains = ipUserRuleList.contains( checkPair );
        if ( contains )
        {
            return AccessType.ACCESS_DENIED;
        }
        return AccessType.ACCESS_GRANTED;
    }

    private void loadHostileHostList( Map<String, DIpAccessRule> systemRuleMap )
    {
        if ( !SecurityPrefs.LoadHostileHostList.get().booleanValue() )
        {
            return;
        }
        try
        {
            NLogger.debug( NLoggerNames.Security,
                "Load hostile hosts file." );
            long start = System.currentTimeMillis();
            InputStream inStream = ClassLoader.getSystemResourceAsStream(
                "phex/resources/hostiles/gtkg-hostiles.txt" );
                //"phex/resources/hostilehosts.gz" );
            BufferedReader br;
            if ( inStream != null )
            {
                br = new BufferedReader( new InputStreamReader( /*new GZIPInputStream(*/ inStream /*)*/ ) );
            }
            else
            {
                NLogger.debug( NLoggerNames.Security,
                    "Hostile hosts file not found." );
                return;
            }

            String line;
            int ip;
            byte cidr;
            IpSystemSecurityRule rule;
            while ( (line = br.readLine()) != null)
            {
                if ( StringUtils.isEmpty(line) || line.startsWith("#") )
                {
                    continue;
                }
                int slashIdx = line.indexOf( '/' );
                
                if ( slashIdx == -1 )
                {// single ip...
                    ip = AddressUtils.parseDottedIpToInt( line );
                    cidr = 32;
                }
                else
                {
                    String ipStr = line.substring( 0, slashIdx ).trim();
                    String extensionStr = line.substring( slashIdx + 1 ).trim();
                    ip = AddressUtils.parseDottedIpToInt( ipStr );
                    cidr = AddressUtils.parseNetmaskToCidr( extensionStr );
                }
                rule = new IpSystemSecurityRule( ip, cidr );
                
                // adjust hit count..
                DSecurityRule xjbRule = findSystemXJBRule( systemRuleMap, ip,
                    cidr );
                if ( xjbRule != null )
                {
                    rule.setTriggerCount( xjbRule.getTriggerCount() );
                }
                ipAccessRuleList.add( rule );
                ipSystemRuleList.add( rule );
                //if ( ipAccessRuleList.size()%10000==0)
                //{
                //    long end = System.currentTimeMillis();
                //    NLogger.debug( NLoggerNames.Security,
                //        "Part: " + ((double)(end-start)/(double)ipAccessRuleList.size()) + " " + ipAccessRuleList.size() );
                //}
            }
            br.close();
            long end = System.currentTimeMillis();
            NLogger.debug( NLoggerNames.Security,
                "Loaded hostile hosts file: " + (end-start) );
        }
        catch ( IOException exp )
        {
            NLogger.warn( NLoggerNames.Security, exp, exp );
        }
    }

    private DSecurityRule findSystemXJBRule( Map<String, DIpAccessRule> systemRuleMap, int ip, 
        byte cidr )
    {
        DSecurityRule xjbRule = systemRuleMap.get( 
            AddressUtils.ip2string(ip) + "/" + cidr );
        if ( xjbRule == null || !xjbRule.isSystemRule())
        {
            return null;
        }
        return xjbRule;
    }

    private void loadSecurityRuleList()
    {
        NLogger.debug( NLoggerNames.Security,
            "Loading security rule list..." );
        File securityFile = Environment.getInstance().getPhexConfigFile(
            EnvironmentConstants.XML_SECURITY_FILE_NAME );
        
        DPhex dPhex;
        try
        {
            if ( securityFile.exists() )
            {
                FileManager fileMgr = FileManager.getInstance();
                ManagedFile managedFile = fileMgr.getReadWriteManagedFile( securityFile );
                dPhex = XMLBuilder.loadDPhexFromFile( managedFile );
            }
            else
            {
                dPhex = new DPhex();
            }            
            DSecurity dSecurity = dPhex.getSecurityList();
            if ( dSecurity == null )
            {
                NLogger.debug( NLoggerNames.Security,
                    "No security definition found." );
                dSecurity = new DSecurity();
            }
            
            synchronized( ipAccessRuleList )
            {
                List<DSecurityRule> dRuleList = dSecurity.getIpAccessRuleList();
                Map<String, DIpAccessRule> systemRuleMap = new HashMap<String, DIpAccessRule>();
                for( DSecurityRule dRule : dRuleList )
                {
                    // currently we only have DIpAccessRules
                    DIpAccessRule dIpRule = (DIpAccessRule)dRule;
                    if( dIpRule.hasDenyingRule() && !dIpRule.isDenyingRule() )
                    {
                        // currently we dont support ACCEPT IP rules
                        // this is done to improve performance in the lookup 
                        // process. A different solution would be to inverse
                        // all ACCEPT rules to achieve the same effect. Is it 
                        // worth the extra work? TODO3
                        continue;
                    }
                    if ( !dRule.isSystemRule() )
                    {
                        if ( dIpRule.hasCidr() )
                        {
                            IpUserSecurityRule rule = new IpUserSecurityRule( 
                                dIpRule.getDescription(), dIpRule.getIp(),
                                dIpRule.getCidr(), dIpRule.isDisabled(),
                                dIpRule.isDeletedOnExpiry(), dIpRule.getExpiryDate() );
                            rule.setTriggerCount( dIpRule.getTriggerCount() );
                            ipAccessRuleList.add( rule );
                            ipUserRuleList.add( rule );
                        }
                        else
                        {
                            if ( dIpRule.getAddressType() == DIpAccessRule.NETWORK_MASK  )
                            {
                                IpUserSecurityRule rule = new IpUserSecurityRule( 
                                    dIpRule.getDescription(), dIpRule.getIp(), 
                                    AddressUtils.calculateCidr( dIpRule.getCompareIp() ),
                                    dIpRule.isDisabled(), dIpRule.isDeletedOnExpiry(), 
                                    dIpRule.getExpiryDate() );
                                rule.setTriggerCount( dIpRule.getTriggerCount() );
                                ipAccessRuleList.add( rule );
                                ipUserRuleList.add( rule );
                            }
                            else if ( dIpRule.getAddressType() == DIpAccessRule.SINGLE_ADDRESS  )
                            {
                                IpUserSecurityRule rule = new IpUserSecurityRule( 
                                    dIpRule.getDescription(), dIpRule.getIp(), 
                                    (byte)32, dIpRule.isDisabled(),
                                    dIpRule.isDeletedOnExpiry(), dIpRule.getExpiryDate() );
                                rule.setTriggerCount( dIpRule.getTriggerCount() );
                                ipAccessRuleList.add( rule );
                                ipUserRuleList.add( rule );
                            }
                            else if ( dIpRule.getAddressType() == DIpAccessRule.NETWORK_RANGE  )
                            {
                                
                                List<IpCidrPair> pairList = AddressUtils.range2cidr( 
                                    dIpRule.getIp(), dIpRule.getCompareIp() );
                                for ( IpCidrPair pair : pairList )
                                {
                                    IpUserSecurityRule rule = new IpUserSecurityRule(
                                        dIpRule.getDescription(), pair.ipAddr, 
                                        pair.cidr, dIpRule.isDisabled(),
                                        dIpRule.isDeletedOnExpiry(), dIpRule.getExpiryDate() );
                                    rule.setTriggerCount( dIpRule.getTriggerCount() );
                                    ipAccessRuleList.add( rule );
                                    ipUserRuleList.add( rule );
                                }
                            }
                        }
                    }
                    else
                    {
                        if ( dIpRule.hasCidr() )
                        {
                            String keyStr = AddressUtils.ip2string( dIpRule.getIp() ) 
                                + "/" + String.valueOf( dIpRule.getCidr() );
                            systemRuleMap.put( keyStr, dIpRule);
                        }
                        else
                        {
                            StringBuffer keyBuf = new StringBuffer( AddressUtils.ip2string( dIpRule.getIp() ) );
                            keyBuf.append( "/" );
                            if ( dIpRule.getCompareIp() == null )
                            {
                                keyBuf.append( "32" );
                            }
                            else
                            {
                                keyBuf.append( AddressUtils.calculateCidr( 
                                        dIpRule.getCompareIp() ) );
                            }
                            systemRuleMap.put( keyBuf.toString(), dIpRule);
                        }
                    }
                }
                loadHostileHostList( systemRuleMap );
                
                // optimize ipAccessRuleList
                ipAccessRuleList.trimToSize();
            }
        }
        catch ( IOException exp )
        {
            NLogger.error( PhexSecurityManager.class, exp, exp );
            Environment.getInstance().fireDisplayUserMessage( 
                UserMessageListener.SecuritySettingsLoadFailed, 
                new String[]{ exp.toString() } );
            return;
        }
        catch ( ManagedFileException exp )
        {
            NLogger.error( PhexSecurityManager.class, exp, exp );
            Environment.getInstance().fireDisplayUserMessage( 
                UserMessageListener.SecuritySettingsLoadFailed, 
                new String[]{ exp.toString() } );
            return;
        }
    }

    private void saveSecurityRuleList()
    {
        NLogger.debug( NLoggerNames.Security, "Saving security rule list..." );

        try
        {
            DPhex dPhex = new DPhex();
            dPhex.setPhexVersion( VersionUtils.getFullProgramVersion() );
            
            DSecurity security = new DSecurity();
            dPhex.setSecurityList( security );

            synchronized( ipAccessRuleList )
            {
                for ( IpSecurityRule rule : ipAccessRuleList )
                {
                    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;
                    }

                    if ( rule.isSystemRule() && rule.getTriggerCount() == 0)
                    {// we dont care about system rules with no trigger count.
                        continue;
                    }
                    
                    DSecurityRule dRule = rule.createDSecurityRule();
                    security.getIpAccessRuleList().add( dRule );
                }
            }

            File securityFile = Environment.getInstance().getPhexConfigFile(
                EnvironmentConstants.XML_SECURITY_FILE_NAME );
            ManagedFile managedFile = FileManager.getInstance().getReadWriteManagedFile( securityFile );
            XMLBuilder.saveToFile( managedFile, dPhex );
        }
        catch ( IOException exp )
        {
            // TODO during close this message is never displayed since application
            // will exit too fast. A solution to delay exit process in case 
            // SlideInWindows are open needs to be found.
            NLogger.error( PhexSecurityManager.class, exp, exp );
            Environment.getInstance().fireDisplayUserMessage( 
                UserMessageListener.SecuritySettingsSaveFailed, 
                new String[]{ exp.toString() } );
        }
        catch ( ManagedFileException exp )
        {
            // TODO during close this message is never displayed since application
            // will exit too fast. A solution to delay exit process in case 
            // SlideInWindows are open needs to be found.
            NLogger.error( PhexSecurityManager.class, exp, exp );
            Environment.getInstance().fireDisplayUserMessage( 
                UserMessageListener.SecuritySettingsSaveFailed, 
                new String[]{ exp.toString() } );
        }
    }

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

    static private class Holder
    {
       static protected final PhexSecurityManager manager = new PhexSecurityManager();
    }

    static public PhexSecurityManager getInstance()
    {
        return PhexSecurityManager.Holder.manager;
    }


    /**
     * 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.
     */
    @Override
    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.
     */
    @Override
    public boolean onPostInitialization()
    {
        loadSecurityRuleList();
        loadHostileSha1List();
        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.
     */
    @Override
    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.
     */
    @Override
    public void shutdown()
    {
        saveSecurityRuleList();
    }
    //////////////////////// End Manager interface ///////////////////////////

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

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

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

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

    private void fireSecurityRuleChanged( final int position )
    {
        ThreadPool.getInstance().addJob(
            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 );
                    }
                }
            }, "SecurityRuleChangeEvent" );
    }

    private void fireSecurityRuleAdded( final SecurityRule rule, final int position )
    {
        ThreadPool.getInstance().addJob(
            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( rule, position );
                    }
                }
            }, "SecurityRuleAddedEvent");
    }

    private void fireSecurityRuleRemoved( final int position )
    {
        ThreadPool.getInstance().addJob(
            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 );
                    }
                }
            }, "SecurityRuleRemovedEvent");
    }

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