/*
 * File    : TRTrackerServerImpl.java
 * Created : 19-Jan-2004
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * 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.
 *
 * 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 ( see the LICENSE file ).
 *
 * 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
 */

package org.gudy.azureus2.core3.tracker.server.impl;

/**
 * @author parg
 *
 */


import java.util.*;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLDecoder;

import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.core3.config.*;
import org.gudy.azureus2.core3.ipfilter.*;
import org.gudy.azureus2.core3.tracker.server.*;

import com.aelitis.azureus.core.util.CopyOnWriteList;

public abstract class 
TRTrackerServerImpl 
	implements 	TRTrackerServer
{
	public static final int RETRY_MINIMUM_SECS			= 60;
	public static final int RETRY_MINIMUM_MILLIS		= RETRY_MINIMUM_SECS*1000;
	public static final int CLIENT_TIMEOUT_MULTIPLIER	= 3;
	
	public static final int TIMEOUT_CHECK 				= RETRY_MINIMUM_MILLIS*CLIENT_TIMEOUT_MULTIPLIER;
	
	public static int		max_peers_to_send			= 0;
	public static boolean	send_peer_ids				= true;
	public static int		announce_cache_period		= TRTrackerServer.DEFAULT_ANNOUNCE_CACHE_PERIOD;
	public static int		scrape_cache_period			= TRTrackerServer.DEFAULT_SCRAPE_CACHE_PERIOD;
	public static int		announce_cache_threshold	= TRTrackerServer.DEFAULT_ANNOUNCE_CACHE_PEER_THRESHOLD;
	public static int		max_seed_retention			= 0;
	public static int		seed_limit					= 0;
	public static boolean	full_scrape_enable			= true;
	public static boolean	restrict_non_blocking_requests	= true;
	
	public static boolean	all_networks_permitted		= true;
	public static String[]	permitted_networks			= {};
	
	public static boolean	support_experimental_extensions;
	
	public static String	redirect_on_not_found		= "";
	
		// torrent map is static across all protocol servers
	
	private static Map		torrent_map = new HashMap(); 
	
	private static Map		link_map	= new HashMap();
	
	protected AEMonitor class_mon 	= new AEMonitor( "TRTrackerServer:class" );


	static{

		COConfigurationManager.addListener(
			new COConfigurationListener()
			{
				public void
				configurationSaved()
				{
					readConfig();
				}
			});
		
		readConfig();
	}

	protected static void
	readConfig()
	{
		send_peer_ids = COConfigurationManager.getBooleanParameter( "Tracker Send Peer IDs" );
				
		max_peers_to_send = COConfigurationManager.getIntParameter( "Tracker Max Peers Returned" );
				
		scrape_cache_period = COConfigurationManager.getIntParameter( "Tracker Scrape Cache", TRTrackerServer.DEFAULT_SCRAPE_CACHE_PERIOD );
				
		announce_cache_period = COConfigurationManager.getIntParameter( "Tracker Announce Cache", TRTrackerServer.DEFAULT_ANNOUNCE_CACHE_PERIOD );
						
		announce_cache_threshold = COConfigurationManager.getIntParameter( "Tracker Announce Cache Min Peers", TRTrackerServer.DEFAULT_ANNOUNCE_CACHE_PEER_THRESHOLD );

		max_seed_retention = COConfigurationManager.getIntParameter( "Tracker Max Seeds Retained", 0 );
		
		seed_limit = COConfigurationManager.getIntParameter( "Tracker Max Seeds", 0 );
		
		List	nets = new ArrayList();
		
		for (int i=0;i<AENetworkClassifier.AT_NETWORKS.length;i++){
			
			String	net = AENetworkClassifier.AT_NETWORKS[i];
			
			boolean	enabled = 
				COConfigurationManager.getBooleanParameter(
						"Tracker Network Selection Default." + net );
			
			if ( enabled ){
				
				nets.add( net );
			}
		}
		
		String[]	s_nets = new String[nets.size()];
		
		nets.toArray(s_nets);
		
		permitted_networks	= s_nets;
		
		all_networks_permitted = s_nets.length == AENetworkClassifier.AT_NETWORKS.length;
		
		full_scrape_enable = COConfigurationManager.getBooleanParameter( "Tracker Server Full Scrape Enable" );

		redirect_on_not_found = COConfigurationManager.getStringParameter( "Tracker Server Not Found Redirect" ).trim();
		
		support_experimental_extensions = COConfigurationManager.getBooleanParameter( "Tracker Server Support Experimental Extensions" );
		
		restrict_non_blocking_requests = COConfigurationManager.getBooleanParameter( "Tracker TCP NonBlocking Restrict Request Types" );
	}
	
	protected static boolean
	getSendPeerIds()
	{
		return( send_peer_ids );
	}
	
	protected static int
	getMaxPeersToSend()
	{
		return( max_peers_to_send );
	}
	
	protected static int
	getScrapeCachePeriod()
	{
		return( scrape_cache_period );
	}
	
	protected static int
	getAnnounceCachePeriod()
	{
		return( announce_cache_period );
	}
	
	protected static int
	getAnnounceCachePeerThreshold()
	{
		return( announce_cache_threshold );
	}
	
	protected static int
	getMaxSeedRetention()
	{
		return( max_seed_retention );
	}
	
	protected static int
	getSeedLimit()
	{
		return( seed_limit );
	}
	
	public static boolean
	isFullScrapeEnabled()
	{
		return( full_scrape_enable );
	}
	
	protected static boolean
	getAllNetworksSupported()
	{
		return( all_networks_permitted );
	}
	
	protected static String[]
	getPermittedNetworks()
	{
		return( permitted_networks );
	}
	
	public static boolean
	supportsExtensions()
	{
		return( support_experimental_extensions );
	}
	
	protected IpFilter	ip_filter	= IpFilterManagerFactory.getSingleton().getIPFilter();
	
	private long		current_announce_retry_interval;
	private long		current_scrape_retry_interval;
	private long		current_total_clients;
	
	private int		current_min_poll_interval;
	private int		current_min_seed_announce_mult;
	
	private TRTrackerServerStatsImpl	stats = new TRTrackerServerStatsImpl( this );
		
	private String	name;
	private boolean	web_password_enabled;
	private boolean	web_password_https_only;
	
	private boolean	tracker_password_enabled;
	private String	password_user;
	private byte[]	password_pw;
	private boolean	compact_enabled;
	private boolean	key_enabled;
	
	private boolean	enabled	= true;

	private boolean	keep_alive_enabled	= false;
	
	
	protected CopyOnWriteList<TRTrackerServerListener>	listeners 	= new CopyOnWriteList<TRTrackerServerListener>();
	protected CopyOnWriteList<TRTrackerServerListener2>	listeners2 	= new CopyOnWriteList<TRTrackerServerListener2>();
	
	private List<TRTrackerServerAuthenticationListener>		auth_listeners		= new ArrayList<TRTrackerServerAuthenticationListener>();
	
	private Vector<TRTrackerServerRequestListener>	request_listeners 	= new Vector<TRTrackerServerRequestListener>();
	
	protected AEMonitor this_mon 	= new AEMonitor( "TRTrackerServer" );

	private COConfigurationListener		config_listener;
	private boolean						destroyed;
	
	private Set							biased_peers;
	
	private boolean						is_ready;
	
	public
	TRTrackerServerImpl(
		String		_name,
		boolean		_start_up_ready )
	{
		name		= _name==null?DEFAULT_NAME:_name;
		is_ready	= _start_up_ready;
		
		
		config_listener = 
			new COConfigurationListener()
			{
				public void
				configurationSaved()
				{
					readConfigSettings();
				}
			};
		
		COConfigurationManager.addListener( config_listener );

		readConfigSettings();
					
		current_min_poll_interval	= COConfigurationManager.getIntParameter("Tracker Poll Interval Min", DEFAULT_MIN_RETRY_DELAY );
		
		if ( current_min_poll_interval < RETRY_MINIMUM_SECS ){
			
			current_min_poll_interval = RETRY_MINIMUM_SECS;
		}
		
		current_min_seed_announce_mult = COConfigurationManager.getIntParameter( "Tracker Poll Seed Interval Mult" );
		
		current_announce_retry_interval = current_min_poll_interval;		

		int	scrape_percentage 		= COConfigurationManager.getIntParameter("Tracker Scrape Retry Percentage", DEFAULT_SCRAPE_RETRY_PERCENTAGE );		
	
		current_scrape_retry_interval	= (current_announce_retry_interval*scrape_percentage)/100;
		
		Thread timer_thread = 
			new AEThread("TrackerServer:timer.loop")
			{
				public void
				runSupport( )
				{
					timerLoop();
				}
			};
		
		timer_thread.setDaemon( true );
		
		timer_thread.start();
	}
	
	protected void
	readConfigSettings()
	{		
		web_password_enabled 		= COConfigurationManager.getBooleanParameter("Tracker Password Enable Web");
		tracker_password_enabled 	= COConfigurationManager.getBooleanParameter("Tracker Password Enable Torrent");

		web_password_https_only		= COConfigurationManager.getBooleanParameter("Tracker Password Web HTTPS Only");
		
		if ( web_password_enabled || tracker_password_enabled ){
			
			password_user	= COConfigurationManager.getStringParameter("Tracker Username", "");
			password_pw		= COConfigurationManager.getByteParameter("Tracker Password", new byte[0]);
		}
		
		compact_enabled = COConfigurationManager.getBooleanParameter("Tracker Compact Enable" );
		
		key_enabled = COConfigurationManager.getBooleanParameter("Tracker Key Enable Server");
	}

	public void
	setReady()
	{
		is_ready	= true;
	}
	
	public final boolean
	isReady()
	{
		return( is_ready );
	}
	
	public void
	setEnabled(
		boolean	e )
	{
		enabled	= e;
	}
	
	public boolean
	isEnabled()
	{
		return( enabled );
	}
	
	public void
	setEnableKeepAlive(
		boolean	enable )
	{
		keep_alive_enabled = enabled;
	}
	
	public boolean
	isKeepAliveEnabled()
	{
		return( keep_alive_enabled );
	}
	
	public TRTrackerServerTorrent
	addLink(
		String					link,
		TRTrackerServerTorrent	target )
	{
		try{
			class_mon.enter();
			
			return((TRTrackerServerTorrent)link_map.put( link, target ));
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	public void
	removeLink(
		String					link,
		TRTrackerServerTorrent	target )
	{
		try{
			class_mon.enter();
			
			link_map.remove( link );
			
		}finally{
			
			class_mon.exit();
		}	
	}
	
	public void
	setBiasedPeers(
		Set		peers )
	{
		if ( biased_peers != null && peers.equals( biased_peers )){
			
			return;
		}
		
		String	str = "";
		
		Iterator	it = peers.iterator();
		
		while( it.hasNext()){
			
			str += " " + it.next();
		}
		
		System.out.println( "biased peers: " + str );
		
		try{
			class_mon.enter();
		
			biased_peers = new HashSet( peers );
			
			Iterator	tit = torrent_map.values().iterator();
			
			while(tit.hasNext()){
							
				TRTrackerServerTorrentImpl	this_torrent = (TRTrackerServerTorrentImpl)tit.next();
				
				this_torrent.updateBiasedPeers( biased_peers );
			}
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	protected Set
	getBiasedPeers()
	{
		return( biased_peers );
	}
	
	public boolean
	isWebPasswordEnabled()
	{
		return( web_password_enabled || auth_listeners.size() > 0 );
	}
	
	public boolean
	isTrackerPasswordEnabled()
	{
		return( tracker_password_enabled || auth_listeners.size() > 0 );
	}
	
	public boolean
	isWebPasswordHTTPSOnly()
	{
		return( web_password_https_only );
	}
	
	public boolean
	hasExternalAuthorisation()
	{
		return( auth_listeners.size() > 0 );
	}	
	
	public boolean
	hasInternalAuthorisation()
	{
		return( web_password_enabled || tracker_password_enabled );
	}
	
	public boolean
	performExternalAuthorisation(
		InetSocketAddress	remote_ip,
		String				headers,
		URL					resource,
		String				user,
		String				password )
	{
		headers = headers.trim() + "\r\nX-Real-IP: " + remote_ip.getAddress().getHostAddress() + "\r\n\r\n";
		
		for (int i=0;i<auth_listeners.size();i++){
			
			try{
				
				if ( ((TRTrackerServerAuthenticationListener)auth_listeners.get(i)).authenticate( headers, resource, user, password )){
					
					return( true );
				}
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
		
		return( false );
	}
	
	public byte[]
	performExternalAuthorisation(
		URL			resource,
		String		user )
	{
		for (int i=0;i<auth_listeners.size();i++){
			
			try{
				
				byte[] sha_pw =  ((TRTrackerServerAuthenticationListener)auth_listeners.get(i)).authenticate( resource, user );
					
				if ( sha_pw != null ){
					
					return( sha_pw );
				}
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
		
		return( null );
	}
	
	public String
	getName()
	{
		return( name );
	}
	
	public boolean
	isCompactEnabled()
	{
		return( compact_enabled );
	}
	public boolean
	isKeyEnabled()
	{
		return( key_enabled );
	}
	
	public String
	getUsername()
	{
		return( password_user );
	}
	
	public byte[]
	getPassword()
	{
		return( password_pw );
	}
	
	public long
	getMinAnnounceRetryInterval()
	{
		return( current_min_poll_interval );
	}
	
	public long
	getAnnounceRetryInterval(
		TRTrackerServerTorrentImpl	torrent )
	{		
		long	clients = current_total_clients;
		
		if ( clients == 0 ){
			
			return( current_announce_retry_interval );
		}
		
		long	res = ( torrent.getPeerCount() * current_announce_retry_interval ) / clients;
		
		if ( res < current_min_poll_interval ){
			
			res = current_min_poll_interval;
		}
		
		return( res );
	}
	
	public long
	getSeedAnnounceIntervalMultiplier()
	{
		return( current_min_seed_announce_mult );
	}
	
	public long
	getScrapeRetryInterval(
		TRTrackerServerTorrentImpl	torrent )
	{		
		long	clients = current_total_clients;
		
		if ( torrent == null || clients == 0 ){
			
			return( current_scrape_retry_interval );
		}
		
		long	res = ( torrent.getPeerCount() * current_scrape_retry_interval ) / clients;
		
		if ( res < current_min_poll_interval ){
			
			res = current_min_poll_interval;
		}
		
		return( res );
	}
	
	public long
	getMinScrapeRetryInterval()
	{
		return( current_min_poll_interval );
	}
	
	public TRTrackerServerStats
	getStats()
	{
		return( stats );
	}
	
	public void
	updateStats(
		int							request_type,					
		TRTrackerServerTorrentImpl	torrent,
		int							bytes_in,
		int							bytes_out )
	{
			// non-synced, but doesn't really matter too much and we don't want to bring all parallel
			// requests down to a single monitor here
		
		stats.update( request_type, bytes_in, bytes_out );
		
		if ( torrent != null ){

			torrent.updateXferStats( bytes_in, bytes_out );

		}else{

			int	num = torrent_map.size();

				// this gets too expensive to do when we have a lot of torrents so just ignore
				// stats if so
			
			if ( num < 256 ){
				
				try{
					class_mon.enter();
	
	
					if ( num > 0 ){
	
						// full scrape or error - spread the reported bytes across the torrents
	
						int	ave_in	= bytes_in/num;
						int	ave_out	= bytes_out/num;
	
						int	rem_in 	= bytes_in-(ave_in*num);
						int rem_out	= bytes_out-(ave_out*num);
	
						Iterator	it = torrent_map.values().iterator();
	
						while(it.hasNext()){
	
							TRTrackerServerTorrentImpl	this_torrent = (TRTrackerServerTorrentImpl)it.next();
	
							if ( it.hasNext()){
	
								this_torrent.updateXferStats( ave_in, ave_out );
	
							}else{
	
								this_torrent.updateXferStats( ave_in+rem_in, ave_out+rem_out );
	
							}
						}
					}
				}finally{
	
					class_mon.exit();
				}
			}
		}
	}
	
	protected void
	updateTime(
		int		request_type,
		long	time )
	{
		stats.updateTime( request_type, time );
	}
		
	
	protected void
	timerLoop()
	{
		long	time_to_go = TIMEOUT_CHECK;
		
		while( !destroyed ){
			
			try{
				Thread.sleep( RETRY_MINIMUM_MILLIS );
				
				time_to_go -= RETRY_MINIMUM_MILLIS;
				
				// recalc tracker interval every minute
				
				current_min_poll_interval 	= COConfigurationManager.getIntParameter("Tracker Poll Interval Min", DEFAULT_MIN_RETRY_DELAY );
				
				if ( current_min_poll_interval < RETRY_MINIMUM_SECS ){
					
					current_min_poll_interval = RETRY_MINIMUM_SECS;
				}
				
				int	min		= current_min_poll_interval;
				int	max 	= COConfigurationManager.getIntParameter("Tracker Poll Interval Max", DEFAULT_MAX_RETRY_DELAY );
				int	inc_by 	= COConfigurationManager.getIntParameter("Tracker Poll Inc By", DEFAULT_INC_BY );
				int	inc_per = COConfigurationManager.getIntParameter("Tracker Poll Inc Per", DEFAULT_INC_PER );
				
				int	scrape_percentage = COConfigurationManager.getIntParameter("Tracker Scrape Retry Percentage", DEFAULT_SCRAPE_RETRY_PERCENTAGE );
				
				int	retry = min;
				
				int	clients = 0;
				
				try{
					class_mon.enter();
					
					Iterator	it = torrent_map.values().iterator();
					
					while(it.hasNext()){
												
						TRTrackerServerTorrentImpl	t = (TRTrackerServerTorrentImpl)it.next();
						
						clients += t.getPeerCount();
					}
				}finally{
					
					class_mon.exit();
				}
				
				if ( inc_by > 0 && inc_per > 0 ){
					
					retry += inc_by * (clients/inc_per);
				}
				
				if ( max > 0 && retry > max ){
					
					retry = max;
				}
				
				if ( retry < RETRY_MINIMUM_SECS ){
					
					retry = RETRY_MINIMUM_SECS;
				}
				
				current_announce_retry_interval = retry;
				
				current_scrape_retry_interval	= (current_announce_retry_interval*scrape_percentage)/100;
				
				current_total_clients	= clients;
				
				// timeout dead clients
				
				if ( time_to_go <= 0 ){
					
					time_to_go = TIMEOUT_CHECK;
					
					try{
						class_mon.enter();
						
						Iterator	it = torrent_map.values().iterator();
						
						while(it.hasNext()){
														
							TRTrackerServerTorrentImpl	t = (TRTrackerServerTorrentImpl)it.next();
							
							t.checkTimeouts();
						}
					}finally{
						
						class_mon.exit();
					}
				}
				
			}catch( InterruptedException e ){
				
				Debug.printStackTrace( e );
			}
			
		}
	}
	
	public TRTrackerServerTorrent
	permit(
		String		_originator,
		byte[]		_hash,
		boolean		_explicit )
	
		throws TRTrackerServerException
	{
		return( permit( _originator, _hash, _explicit, true ));
	}
	
	public TRTrackerServerTorrent
	permit(
		String		_originator,
		byte[]		_hash,
		boolean		_explicit,
		boolean		_enabled )
	
		throws TRTrackerServerException
	{
		// System.out.println( "TRTrackerServerImpl::permit( " + _explicit + ")");
		
		HashWrapper	hash = new HashWrapper( _hash );
		
			// don't invoke listeners when synched, deadlock possible
		
		TRTrackerServerTorrentImpl	entry;
		
		try{
			class_mon.enter();
			
			entry = (TRTrackerServerTorrentImpl)torrent_map.get( hash );
			
		}finally{
			
			class_mon.exit();
		}
		
		if ( entry == null ){
			
			Iterator<TRTrackerServerListener>	it = listeners.iterator();
			
			while( it.hasNext()){
						
				if ( !it.next().permitted( _originator, _hash, _explicit )){
					
					throw( new TRTrackerServerException( "operation denied"));			
				}
			}
		
			try{
				class_mon.enter();
			
					// double check in-case added in parallel
				
				entry = (TRTrackerServerTorrentImpl)torrent_map.get( hash );
				
				if ( entry == null ){
				
					entry = new TRTrackerServerTorrentImpl( this, hash, _enabled );
				
					torrent_map.put( hash, entry );
				}
			}finally{
				
				class_mon.exit();
			}
		}
		
		return( entry );
	}
	
	public void
	deny(
		byte[]		_hash,
		boolean		_explicit )
	
		throws TRTrackerServerException
	{
		// System.out.println( "TRTrackerServerImpl::deny( " + _explicit + ")");
		
		HashWrapper	hash = new HashWrapper( _hash );
		
		Iterator<TRTrackerServerListener>	it = listeners.iterator();
			
		while( it.hasNext()){
			
			if ( !it.next().denied( _hash, _explicit )){				
				
				throw( new TRTrackerServerException( "operation denied"));			
			}
		}

		try{
			class_mon.enter();
			
			TRTrackerServerTorrentImpl	entry = (TRTrackerServerTorrentImpl)torrent_map.get( hash );
	
			if ( entry != null ){
				
				entry.delete();
			}
		
			torrent_map.remove( hash );
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	public TRTrackerServerTorrentImpl
	getTorrent(
		byte[]		hash )
	{
		try{
			class_mon.enter();
		
			return((TRTrackerServerTorrentImpl)torrent_map.get(new HashWrapper(hash)));
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	public TRTrackerServerTorrentImpl
	getTorrent(
		String		link )
	{
		try{
			class_mon.enter();
		
			return((TRTrackerServerTorrentImpl)link_map.get( link ));
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	public TRTrackerServerTorrentImpl[]
	getTorrents()
	{
		try{
			class_mon.enter();
		
			TRTrackerServerTorrentImpl[]	res = new TRTrackerServerTorrentImpl[torrent_map.size()];
			
			torrent_map.values().toArray( res );
			
			return( res );	
		}finally{
			
			class_mon.exit();
		}
	}
	
	public int
	getTorrentCount()
	{
		return( torrent_map.size());
	}
	
	public TRTrackerServerTorrentStats
	getStats(
		byte[]		hash )
	{
		TRTrackerServerTorrentImpl	torrent = getTorrent( hash );
		
		if ( torrent == null ){
			
			return( null );
		}
		
		return( torrent.getStats());
	}	
	
	public TRTrackerServerPeer[]
	getPeers(
		byte[]		hash )
	{
		TRTrackerServerTorrentImpl	torrent = getTorrent( hash );
		
		if ( torrent == null ){
			
			return( null );
		}
		
		return( torrent.getPeers());
	}
	
	public void
	addListener(
		TRTrackerServerListener	l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		TRTrackerServerListener	l )
	{
		listeners.remove(l);
	}
	
	public void
	addListener2(
		TRTrackerServerListener2	l )
	{
		listeners2.add( l );
	}
	
	public void
	removeListener2(
		TRTrackerServerListener2	l )
	{
		listeners2.remove(l);
	}
	
	public void
	addAuthenticationListener(
		TRTrackerServerAuthenticationListener	l )
	{
		auth_listeners.add( l );
	}
	
	public void
	removeAuthenticationListener(
		TRTrackerServerAuthenticationListener	l )
	{
		auth_listeners.remove(l);
	}
	
	public void
	preProcess(
		TRTrackerServerPeer			peer,
		TRTrackerServerTorrent		torrent,
		int							type,
		String						request,
		Map							response )
	
		throws TRTrackerServerException
	{
		if ( request_listeners.size() > 0 ){
			
				// if this is a scrape then we need to patch up stuff as it may be multi-scrape
			
			if ( type == TRTrackerServerRequest.RT_SCRAPE ){
				
				try{
					int	request_pos = 10;
					
					while( true ){
						
						int	p = request.indexOf( "info_hash=", request_pos );
						
						String	bit;
						
						if ( p == -1 ){
						
							if ( request_pos == 10 ){
								
								break;	// only one entry, nothing to do
							}
							
							bit = request.substring( request_pos );
							
						}else{
							
							bit = request.substring( request_pos, p );
						}
														
						int	pos = bit.indexOf('&');
						
						String	hash_str = pos==-1?bit:bit.substring(0,pos);
						
						hash_str = URLDecoder.decode( hash_str, Constants.BYTE_ENCODING );
						
						byte[]	hash = hash_str.getBytes(Constants.BYTE_ENCODING);
						
						if ( Arrays.equals( hash, torrent.getHash().getBytes())){
							
							request = "info_hash=" + bit;
							
							if ( request.endsWith("&")){
								
								request = request.substring(0,request.length()-1);
							}
							
							break;
						}
						
						if ( p == -1 ){
							
							break;
						}
						
						request_pos = p + 10;
					}
				}catch( Throwable e ){
						
					Debug.printStackTrace(e);
				}
			}
			
			TRTrackerServerRequestImpl	req = new TRTrackerServerRequestImpl( this, peer, torrent, type, request, response );
			
			for (int i=0;i<request_listeners.size();i++){
				
				try{
					((TRTrackerServerRequestListener)request_listeners.elementAt(i)).preProcess( req );
					
				}catch( TRTrackerServerException e ){
				
					throw( e );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace( e );
				}
			}
		}
	}
	
	public void
	postProcess(
		TRTrackerServerPeer			peer,
		TRTrackerServerTorrentImpl	torrent,
		int							type,
		String						request,
		Map							response )
	
		throws TRTrackerServerException
	{
		if ( request_listeners.size() > 0 ){
			
			TRTrackerServerRequestImpl	req = new TRTrackerServerRequestImpl( this, peer, torrent, type, request, response );
			
			for (int i=0;i<request_listeners.size();i++){
				
				try{
					((TRTrackerServerRequestListener)request_listeners.elementAt(i)).postProcess( req );
				
				}catch( TRTrackerServerException e ){
					
					throw( e );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace( e );
				}
			}
		}
	}
		
	public void
	addRequestListener(
		TRTrackerServerRequestListener	l )
	{
		request_listeners.addElement( l );
	}
	
	public void
	removeRequestListener(
		TRTrackerServerRequestListener	l )
	{
		request_listeners.removeElement(l);
	}
	
	public void
	close()
	{
		TRTrackerServerFactoryImpl.close( this );
	}
	
	protected abstract void
	closeSupport();
	
	protected void
	destroySupport()
	{
		destroyed	= true;
		
		COConfigurationManager.removeListener( config_listener );
	}
}
