/*
 * Created on Dec 5, 2012
 * 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.pairing.impl;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.AlgorithmParameters;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.crypto.agreement.srp.SRP6Server;
import org.bouncycastle.crypto.agreement.srp.SRP6VerifierGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.RandomUtils;
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.core3.util.UrlUtils;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageRequest;
import org.gudy.azureus2.plugins.tracker.web.TrackerWebPageResponse;
import org.json.simple.JSONObject;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.nat.DHTNATPuncher;
import com.aelitis.azureus.core.dht.nat.DHTNATPuncherListener;
import com.aelitis.azureus.core.dht.transport.DHTTransportContact;
import com.aelitis.azureus.core.nat.NATTraversalHandler;
import com.aelitis.azureus.core.nat.NATTraverser;
import com.aelitis.azureus.core.pairing.PairedServiceRequestHandler;
import com.aelitis.azureus.core.pairing.impl.PairingManagerImpl.PairedServiceImpl;
import com.aelitis.azureus.core.security.CryptoManager;
import com.aelitis.azureus.core.security.CryptoManagerFactory;
import com.aelitis.azureus.plugins.dht.DHTPlugin;
import com.aelitis.azureus.util.JSONUtils;

public class 
PairingManagerTunnelHandler 
{
	private static final String DEFAULT_IDENTITY = "vuze";
	
	private BigInteger N_3072;
	private BigInteger G_3072;

	private byte[] 		SRP_SALT;
	private BigInteger 	SRP_VERIFIER;

	private PairingManagerImpl		manager;
	private AzureusCore				core;
	
	private boolean	started = false;
	private boolean	active	= false;
	
	private List<DHTNATPuncher>	nat_punchers_ipv4 = new ArrayList<DHTNATPuncher>();
	private List<DHTNATPuncher>	nat_punchers_ipv6 = new ArrayList<DHTNATPuncher>();
	
	private int	last_punchers_registered = 0;
	
	private TimerEvent	update_event;
	

	private Map<String,Object[]> local_server_map = 
		new LinkedHashMap<String,Object[]>( 10, 0.75f, true )
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry<String,Object[]> eldest) 
			{
				return size() > 10;
			}
		};
	
	private long 	last_server_create_time;
	private long 	last_server_agree_time;
	private int		total_servers;
		
	private long 	last_local_server_create_time;
	private long 	last_local_server_agree_time;	
	private int		total_local_servers;
	
	private static final int MAX_TUNNELS	= 10;
	
	private Map<String,PairManagerTunnel>	tunnels = new HashMap<String,PairManagerTunnel>();
	
	private String	init_fail;
	
	
	protected
	PairingManagerTunnelHandler(
		PairingManagerImpl	_manager,
		AzureusCore			_core )
	{
		manager		= _manager;
		core		= _core;
		
		CryptoManager.SRPParameters params = CryptoManagerFactory.getSingleton().getSRPParameters();
		
		if ( params != null ){
			
			SRP_SALT		= params.getSalt();
			SRP_VERIFIER	= params.getVerifier();
		}
	}
	
	public void
	setSRPPassword(
		char[]		password )
	{
		if ( password == null || password.length == 0 ){
			
			SRP_SALT		= null;
			SRP_VERIFIER	= null;
				
			CryptoManagerFactory.getSingleton().setSRPParameters(  null, null );
			
		}else{
			
			start();
			
			try{
				byte[] I = DEFAULT_IDENTITY.getBytes( "UTF-8" );
				byte[] P = new String(password).getBytes( "UTF-8" );
		
				byte[] salt = new byte[16];
		
				RandomUtils.nextSecureBytes( salt );
		
				SRP6VerifierGenerator gen = new SRP6VerifierGenerator();
		
				gen.init( N_3072, G_3072, new SHA256Digest());
		
				BigInteger verifier = gen.generateVerifier( salt, I, P );
				
				CryptoManagerFactory.getSingleton().setSRPParameters( salt, verifier );
				
				SRP_SALT		= salt;
				SRP_VERIFIER 	= verifier;
				
			}catch( Throwable e ){
				
				Debug.out( e );
			}
		}
		
		updateActive();
	}
	
	private void
	start()
	{
		synchronized( this ){
			
			if ( started ){
				
				return;
			}
			
			started = true;
		}
		
		N_3072 = fromHex(
				"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
				"8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
				"302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
				"A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
				"49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
				"FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
				"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
				"180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
				"3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
				"04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
				"B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
				"1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
				"BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
				"E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF" );

		G_3072 = BigInteger.valueOf(5);

		try{		
			PluginInterface dht_pi = core.getPluginManager().getPluginInterfaceByClass( DHTPlugin.class );
			
			if ( dht_pi == null ){
				
				throw( new Exception( "DHT Plugin not found" ));
			}
			
			DHTPlugin dht_plugin = (DHTPlugin)dht_pi.getPlugin();
			
			if ( !dht_plugin.isEnabled()){
				
				throw( new Exception( "DHT Plugin is disabled" ));

			}
			
			DHT[] dhts = dht_plugin.getDHTs();
			
			List<DHTNATPuncher> punchers = new ArrayList<DHTNATPuncher>();
			
			for ( DHT dht: dhts ){
				
				int net = dht.getTransport().getNetwork();
																	
				if ( net == DHT.NW_MAIN ){
					
					DHTNATPuncher primary_puncher = dht.getNATPuncher();
					
					if ( primary_puncher != null ){
						
						punchers.add( primary_puncher );
						
						nat_punchers_ipv4.add( primary_puncher );
						
						for ( int i=1;i<=2; i++ ){
							
							DHTNATPuncher puncher = primary_puncher.getSecondaryPuncher();
						
							punchers.add( puncher );
						
							nat_punchers_ipv4.add( puncher );
						}
					}
				}else if ( net == DHT.NW_MAIN_V6 ){
					
					/*
					 * no point in this atm as we don't support v6 tunnels

					DHTNATPuncher puncher = dht.getNATPuncher();
					
					if ( puncher != null ){
						
						punchers.add( puncher );
						
						nat_punchers_ipv6.add( puncher );
						
						puncher = puncher.getSecondaryPuncher();
						
						punchers.add( puncher );
						
						nat_punchers_ipv6.add( puncher );
					}
					*/
				}
			}
		
			if ( punchers.size() == 0 ){
				
				throw( new Exception( "No suitable DHT instances available" ));
			}
			
			for ( DHTNATPuncher p: punchers ){
		
				p.forceActive( true );
						
				p.addListener(
					new DHTNATPuncherListener()
					{
						public void 
						rendezvousChanged(
							DHTTransportContact rendezvous )
						{
							System.out.println( "active: " + rendezvous.getString());
							
							synchronized( PairingManagerTunnelHandler.this ){
								
								if ( update_event == null ){
									
									update_event = 
										SimpleTimer.addEvent(
											"PMT:defer",
											SystemTime.getOffsetTime( 15*1000 ),
											new TimerEventPerformer()
											{
												public void 
												perform(
													TimerEvent event) 
												{
													synchronized( PairingManagerTunnelHandler.this ){
														
														update_event = null;
													}
													
													System.out.println( "    updating" );
													
													manager.updateNeeded();
												};	
											});
								}
							}
						}
					});										
			}
			
			core.getNATTraverser().registerHandler( 
				new NATTraversalHandler()
				{
					private Map<Long,Object[]> server_map = 
						new LinkedHashMap<Long,Object[]>( 10, 0.75f, true )
						{
							protected boolean 
							removeEldestEntry(
						   		Map.Entry<Long,Object[]> eldest) 
							{
								return size() > 10;
							}
						};
											
					public int
					getType()
					{
						return( NATTraverser.TRAVERSE_REASON_PAIR_TUNNEL );
					}
					
					public String
					getName()
					{
						return( "Pairing Tunnel" );
					}
					
					public Map
					process(
						InetSocketAddress	originator,
						Map					data )
					{
						if ( SRP_VERIFIER == null || !active ){
							
							return( null );
						}
						
						boolean	good_request = false;
												
						try{
						
							Map result = new HashMap();
							
							Long session = (Long)data.get( "sid" );
							
							if ( session == null ){
								
								return( null );
							}
							
							InetAddress	tunnel_originator;
							
							try{
								tunnel_originator = InetAddress.getByAddress( (byte[])data.get( "origin"));
								
							}catch( Throwable e ){
								
								Debug.out( "originator decode failed: " + data );
								
								return( null );
							}
		
							System.out.println( "PairManagerTunnelHander: incoming message - session=" + session + ", payload=" + data + " from " + tunnel_originator + " via " + originator);
							
							SRP6Server	server;
							BigInteger	B;
							
							synchronized( server_map ){
								
								Object[] entry = server_map.get( session );
								
								if ( entry == null ){
																	
									long diff = SystemTime.getMonotonousTime() - last_server_create_time;
									
									if ( diff < 5000 ){
									
										try{
											long	sleep = 5000 - diff;
											
											System.out.println( "Sleeping for " + sleep + " before starting srp" );
											
											Thread.sleep( sleep );
											
										}catch( Throwable e ){
										}
									}
																		
									server = new SRP6Server();
									
							        server.init( N_3072, G_3072, SRP_VERIFIER, new SHA256Digest(), RandomUtils.SECURE_RANDOM );
		
							        B = server.generateServerCredentials();
								        
									server_map.put( session, new Object[]{ server, B });
									
									last_server_create_time = SystemTime.getMonotonousTime();
									
									total_servers++;
	
								}else{
									
									server 	= (SRP6Server)entry[0];
									B		= (BigInteger)entry[1];
								}
							}
							
							Long	op = (Long)data.get( "op" );
							
							if ( op == 1 ){						
		
								result.put( "op", 2 );
								
								result.put( "s", SRP_SALT );
								
						        result.put( "b", B.toByteArray());
						        
						        good_request = true;
						        
						        if ( data.containsKey( "test" )){
						        	
									manager.recordRequest( "SRP Test", originator.getAddress().getHostAddress(), true );

						        }
							}else if ( op == 3 ){
								
								boolean log_error = true;
								
								try{	
									long diff = SystemTime.getMonotonousTime() - last_server_agree_time;
									
									if ( diff < 5000 ){
									
										try{
											long	sleep = 5000 - diff;
											
											System.out.println( "Sleeping for " + sleep + " before completing srp" );
											
											Thread.sleep( sleep );
											
										}catch( Throwable e ){
										}
									}
									
									BigInteger A = new BigInteger((byte[])data.get( "a" ));
															        
							        BigInteger serverS = server.calculateSecret( A );
							        
							        byte[] shared_secret = serverS.toByteArray();
							        
									Cipher decipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
		
									byte[] key = new byte[16];
									
									System.arraycopy( shared_secret, 0, key, 0, 16 );
		
									SecretKeySpec secret = new SecretKeySpec( key, "AES");
									
									decipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec((byte[])data.get( "enc_iv" )));
									
									byte[] dec = decipher.doFinal( (byte[])data.get( "enc_data" ));
									
									String json_str = new String( dec, "UTF-8" );
									
									if ( !json_str.startsWith( "{" )){
										
										log_error = false;
										
										throw( new Exception( "decode failed" ));
									}
									
									JSONObject dec_json = (JSONObject)JSONUtils.decodeJSON( json_str );
																	
									String tunnel_url = (String)dec_json.get( "url" );
									
									String service_id = new String((byte[])data.get( "service"), "UTF-8" );
									
									String endpoint_url = (String)dec_json.get( "endpoint");
									
									boolean	ok = createTunnel( tunnel_originator, session, service_id, secret, tunnel_url, endpoint_url );
		
									result.put( "op", 4 );
									result.put( "status", ok?"ok":"failed" );
									
									good_request = true;
									
								}catch( Throwable e ){
									
									result.put( "op", 4 );
									result.put( "status", "failed" );
									
										// filter usual errors on bad agreement
									
									if ( 	e instanceof BadPaddingException ||
											e instanceof IllegalBlockSizeException ){
										
										log_error = false;
									}
									
									if ( log_error ){
									
										e.printStackTrace();
									}
								}finally{
									
									last_server_agree_time = SystemTime.getMonotonousTime();
								}
							}
							
							return( result );
							
						}finally{
							
							if ( !good_request ){
							
								manager.recordRequest( "SRP", originator.getAddress().getHostAddress(), false );
							}
						}
					}
				});
			
			SimpleTimer.addPeriodicEvent(
				"pm:tunnel:stats",
				30*1000,
				new TimerEventPerformer()
				{
					public void 
					perform(
						TimerEvent event) 
					{
						synchronized( tunnels ){
						
							if ( tunnels.size() > 0 ){
								
								System.out.println( "PairTunnels: " + tunnels.size());
								
								for ( PairManagerTunnel t: tunnels.values()){
								
									System.out.println( "\t" + t.getString());
								}
							}
						}
					}
				});
			
		}catch( Throwable e ){

			Debug.out( e );
			
			init_fail = Debug.getNestedExceptionMessage( e );
			
			manager.updateSRPState();
		}
	}
	
	protected String
	getStatus()
	{
		if ( init_fail != null ){
			
			return( MessageText.getString( "MyTorrentsView.menu.setSpeed.disabled" ) + ": " + init_fail );
			
		}else if ( !active ){
			
			return( MessageText.getString( "pairing.status.initialising" ) + "..." );
			
		}else if ( SRP_SALT == null ){
			
			return( MessageText.getString( "pairing.srp.pw.req" ));
			
		}else if ( last_punchers_registered == 0 ){
			
			return( MessageText.getString( "pairing.srp.registering" ));
			
		}else{
			
			return( MessageText.getString( "tps.status.available" ));
		}
	}
	
	protected void
	setActive(
		boolean	a )
	{
		synchronized( this ){
			
			if ( active == a ){
				
				return;
			}
			
			active	= a;
		}
		
		updateActive();
	}
	
	private void
	updateActive()
	{
		manager.updateSRPState();
		
		if ( active && SRP_VERIFIER != null ){
			
			start();
			
		}else{
			
			synchronized( tunnels ){
			
				for ( PairManagerTunnel t: new ArrayList<PairManagerTunnel>( tunnels.values())){
					
					t.destroy();
				}
			}
			
			
			synchronized( local_server_map ){
				
				local_server_map.clear();
			}
		}
		
		List<DHTNATPuncher> punchers = new ArrayList<DHTNATPuncher>();
		
		punchers.addAll( nat_punchers_ipv4 );
		punchers.addAll( nat_punchers_ipv6 );
		
		for ( DHTNATPuncher p: punchers ){
			
			p.forceActive( active );
		}
	}
	
	protected void
	updateRegistrationData(
		Map<String,Object>		payload )
	{
		int	puncher_num = 0;
	
		int	num_registered = 0;
		
		for ( DHTNATPuncher nat_ipv4: nat_punchers_ipv4 ){
								
			DHTTransportContact rend 	= nat_ipv4.getRendezvous();
			DHTTransportContact lc 		= nat_ipv4.getLocalContact();
	
			if ( rend != null && lc != null ){
			
				puncher_num++;
	
				InetSocketAddress rend_address = rend.getTransportAddress();
				
				num_registered++;
				
				payload.put( "rc_v4-" + puncher_num, rend_address.getAddress().getHostAddress() + ":" + rend_address.getPort());
			
				if ( puncher_num == 1 ){
				
					payload.put( "rl_v4", lc.getExternalAddress().getAddress().getHostAddress() + ":" + lc.getAddress().getPort());
				}
			}
		}
		
		puncher_num = 0;
	
		for ( DHTNATPuncher nat_ipv6: nat_punchers_ipv6 ){
							
			DHTTransportContact rend 	= nat_ipv6.getRendezvous();
			DHTTransportContact lc 		= nat_ipv6.getLocalContact();
	
			if ( rend != null && lc != null ){
				
				puncher_num++;
			
				InetSocketAddress rend_address = rend.getTransportAddress();
				
				num_registered++;
				
				payload.put( "rc_v6-" + puncher_num, rend_address.getAddress().getHostAddress() + ":" + rend_address.getPort());
	
				if ( puncher_num == 1 ){
				
					payload.put( "rl_v6", lc.getExternalAddress().getAddress().getHostAddress() + ":" + lc.getAddress().getPort());
				}
			}
		}
		
		if ( num_registered != last_punchers_registered ){
			
			last_punchers_registered = num_registered;
			
			manager.updateSRPState();
		}
	}
	
	private BigInteger 
	fromHex(
		String hex )
	{
		return new BigInteger(1, ByteFormatter.decodeString( hex.replaceAll( " ", "" )));
	}
	
	protected boolean
	handleLocalTunnel(
		TrackerWebPageRequest		request,
		TrackerWebPageResponse		response )
	
		throws IOException
	{	
		start();
		
		if ( SRP_VERIFIER == null || !active ){
			
			throw( new IOException( "Secure pairing is not enabled" ));
		}
		
		boolean	good_request = false;
		
		try{
				// remove /pairing/tunnel/
					
			String url = request.getURL().substring( 16 );
					
			int	q_pos = url.indexOf( '?' );
			
			Map<String,String> args = new HashMap<String,String>();
			
			if ( q_pos != -1 ){
				
				String	args_str = url.substring( q_pos+1 );
				
				String[]	bits = args_str.split( "&" );
				
				for ( String arg: bits ){
					
					String[] x = arg.split( "=" );
					
					if ( x.length == 2 ){
						
						args.put( x[0].toLowerCase(), x[1] );
					}
				}
				
				url = url.substring( 0, q_pos );
			}
			
			if ( url.startsWith( "create" )){
				
				String	ac 	= args.get( "ac" );
				String	sid	= args.get( "sid" );
				
				if ( ac == null || sid == null ){
					
					throw( new IOException( "Access code or service id missing" ));
				}
				
				if ( !ac.equals( manager.peekAccessCode())){
					
					throw( new IOException( "Invalid access code" ));
				}
				
				PairedServiceImpl ps = manager.getService( sid );
				
				if ( ps == null ){
					
					good_request = true;
					
					throw( new IOException( "Service '" + sid + "' not registered" ));
				}
				
				PairedServiceRequestHandler handler = ps.getHandler();
				
				if ( handler == null ){
				
					good_request = true;
					
					throw( new IOException( "Service '" + sid + "' has no handler registered" ));
				}
				
				JSONObject json = new JSONObject();
				
				JSONObject result = new JSONObject();
				
				json.put( "result", result );
				
				byte[]	ss = new byte[]{ SRP_SALT[0], SRP_SALT[1], SRP_SALT[2], SRP_SALT[3] };
				
				long	tunnel_id = RandomUtils.nextSecureAbsoluteLong();
				
				String	tunnel_name = Base32.encode( ss ) + "_" + tunnel_id;
	
				synchronized( local_server_map ){
																	
					long diff = SystemTime.getMonotonousTime() - last_local_server_create_time;
					
					if ( diff < 5000 ){
					
						try{
							long	sleep = 5000 - diff;
							
							System.out.println( "Sleeping for " + sleep + " before starting srp" );
							
							Thread.sleep( sleep );
							
						}catch( Throwable e ){
						}
					}
															
					SRP6Server server = new SRP6Server();
					
			        server.init( N_3072, G_3072, SRP_VERIFIER, new SHA256Digest(), RandomUtils.SECURE_RANDOM );
	
			        BigInteger B = server.generateServerCredentials();
				        
			        local_server_map.put( tunnel_name, new Object[]{ server, handler, null, null });
					
					last_local_server_create_time = SystemTime.getMonotonousTime();			
						
					total_local_servers++;
					
					result.put( "srp_salt", Base32.encode( SRP_SALT ));
					
			        result.put( "srp_b", Base32.encode( B.toByteArray()));
			        
			        Map<String,String> headers = request.getHeaders();
			        
			        String	host = headers.get( "host" );
			        		
			        	// remove port number
			        
			        int pos = host.lastIndexOf( "]" );
			        
			        if ( pos != -1 ){
			        	
			        		// ipv6 literal
			        	
			        	host = host.substring( 0, pos+1 );
			        	
			        }else{
			        	
			        	pos = host.indexOf( ':' );
			        	
			        	if ( pos != -1 ){
			        		
			        		host = host.substring( 0, pos );
			        	}
			        }
			        
			        String abs_url = request.getAbsoluteURL().toString();
			        
			        	// unfortunately there is some nasty code that uses a configured tracker
			        	// address as the default host
			        
			        abs_url = UrlUtils.setHost( new URL( abs_url ), host).toExternalForm();
			        			        
			        pos = abs_url.indexOf( "/create" );
			        
			        String tunnel_url = abs_url.substring(0,pos) + "/id/" + tunnel_name;
			        
			        result.put( "url", tunnel_url );
				}
				
				response.getOutputStream().write( JSONUtils.encodeToJSON( json ).getBytes( "UTF-8" ));
				
				response.setContentType( "application/json; charset=UTF-8" );
				
				response.setGZIP( true );
				
				good_request = true;
				
				return( true );
				
			}else if ( url.startsWith( "id/" )){
				
				String	tunnel_name = url.substring( 3 );
				
				Object[]	entry;
				
				synchronized( local_server_map ){
	
					entry = local_server_map.get( tunnel_name );
					
					if ( entry == null ){
						
						good_request = true;
						
						throw( new IOException( "Unknown tunnel id" ));
					}
				}
			
				String	srp_a 		= args.get( "srp_a" );
				String	enc_data 	= args.get( "enc_data" );
				String	enc_iv	 	= args.get( "enc_iv" );
				
				if ( srp_a != null && enc_data != null && enc_iv != null ){
					
					try{
						synchronized( local_server_map ){
	
							long diff = SystemTime.getMonotonousTime() - last_local_server_agree_time;
							
							if ( diff < 5000 ){
							
								try{
									long	sleep = 5000 - diff;
									
									System.out.println( "Sleeping for " + sleep + " before completing srp" );
									
									Thread.sleep( sleep );
									
								}catch( Throwable e ){
								}
							}
						}
						
						JSONObject json = new JSONObject();
						
						JSONObject result = new JSONObject();
						
						json.put( "result", result );
	
						SRP6Server server = (SRP6Server)entry[0];
						
						BigInteger A = new BigInteger( Base32.decode( srp_a ));
				        
				        BigInteger serverS = server.calculateSecret( A );
				        
				        byte[] shared_secret = serverS.toByteArray();
				        
						Cipher decipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
	
						byte[] key = new byte[16];
						
						System.arraycopy( shared_secret, 0, key, 0, 16 );
	
						SecretKeySpec secret = new SecretKeySpec( key, "AES");
						
						decipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec( Base32.decode( enc_iv )));
						
						byte[] dec = decipher.doFinal( Base32.decode( enc_data ));
						
						JSONObject dec_json = (JSONObject)JSONUtils.decodeJSON( new String( dec, "UTF-8" ));
														
						String tunnel_url = (String)dec_json.get( "url" );
						
						if ( !tunnel_url.contains( tunnel_name )){
							
							throw( new IOException( "Invalid tunnel url" ));
						}
												
						String endpoint_url = (String)dec_json.get( "endpoint");
						
						entry[2] = secret;
						entry[3] = endpoint_url;
						
						result.put( "state", "activated" );
						
						response.getOutputStream().write( JSONUtils.encodeToJSON( json ).getBytes( "UTF-8" ));
												
						response.setContentType( "application/json; charset=UTF-8" );
						
						response.setGZIP( true );
						
						good_request = true;
						
						return( true );
						
					}catch( Throwable e ){
						
						throw( new IOException( Debug.getNestedExceptionMessage( e )));
						
					}finally{
						
						last_local_server_agree_time = SystemTime.getMonotonousTime();
					}
				}else if ( args.containsKey( "close" )){
					
					synchronized( local_server_map ){
						
						local_server_map.remove( tunnel_name );
					}
					
					good_request = true;
					
					return( true );
					
				}else{
					
					PairedServiceRequestHandler	request_handler = (PairedServiceRequestHandler)entry[1];
					
					SecretKeySpec	secret = (SecretKeySpec)entry[2];
					
					String	endpoint_url = (String)entry[3];
					
					if ( secret == null ){
						
						throw( new IOException( "auth not completed" ));
					}
	
					byte[] request_data = FileUtil.readInputStreamAsByteArray( request.getInputStream());
					
	         		try{
	         			byte[] decrypted;
	         			
	         			{
	         				byte[]	IV = new byte[16];
	         				
	         				System.arraycopy( request_data, 0, IV, 0, IV.length );
	         				
	         				Cipher decipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
	         	
	         				decipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec( IV ));
	         				
	         				decrypted = decipher.doFinal( request_data, 16, request_data.length-16 );
	         			}
	         			
	         			byte[] reply_bytes = request_handler.handleRequest( request.getClientAddress2().getAddress(), endpoint_url, decrypted );			
	
	         			{				 
	         				Cipher encipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
	         				
	         				encipher.init( Cipher.ENCRYPT_MODE, secret );
	         					
	         				AlgorithmParameters params = encipher.getParameters ();
	         					
	         				byte[] IV = params.getParameterSpec(IvParameterSpec.class).getIV();
	         					
	         				byte[] enc = encipher.doFinal( reply_bytes );
	
	         				byte[] rep_bytes = new byte[ IV.length + enc.length ];
	         				
	         				System.arraycopy( IV, 0, rep_bytes, 0, IV.length );
	         				System.arraycopy( enc, 0, rep_bytes, IV.length, enc.length );
	         				
	         				response.getOutputStream().write( rep_bytes );
	         				
	         				response.setContentType( "application/octet-stream" );
	         				
	         				good_request = true;
	         				
	         				return( true );
	         			}
	         		}catch( Throwable e ){
	         			
	         			throw( new IOException( Debug.getNestedExceptionMessage( e )));
	         		}
				}
			}
			
			throw( new IOException( "Unknown tunnel operation" ));
			
		}finally{
			
			if ( !good_request ){
			
				manager.recordRequest( "SRP", request.getClientAddress2().getAddress().getHostAddress(), false );
			}
		}
	}
	
	private boolean
	createTunnel(
		InetAddress		originator,
		long			session,
		String			sid,
		SecretKeySpec	secret,
		String			tunnel_url,
		String			endpoint_url )
	{
		PairedServiceImpl ps = manager.getService( sid );
		
		if ( ps == null ){
			
			Debug.out( "Service '" + sid + "' not registered" );
			
			return( false );
		}
		
		PairedServiceRequestHandler handler = ps.getHandler();
		
		if ( handler == null ){
		
			Debug.out( "Service '" + sid + "' has no handler registered" );
			
			return( false );
		}
				
		String 	key = originator.getHostAddress() + ":" + session + ":" + sid;
		
		synchronized( tunnels ){
			
			PairManagerTunnel existing = tunnels.get( key );
			
			if ( existing != null ){
				
				return( true );
			}
			
			if ( tunnels.size() > MAX_TUNNELS ){
				
				long				oldest_active = Long.MAX_VALUE;
				PairManagerTunnel	oldest_tunnel = null;
				
				for ( PairManagerTunnel t: tunnels.values()){
					
					long	at = t.getLastActive();
					
					if ( at < oldest_active ){
						
						oldest_active 	= at;
						oldest_tunnel	= t;
					}	
				}
				
				oldest_tunnel.destroy();
				
				tunnels.remove( oldest_tunnel.getKey());
			}
			
			PairManagerTunnel tunnel = new PairManagerTunnel( this, key, originator, sid, handler, secret, tunnel_url, endpoint_url );

			tunnels.put( key, tunnel );
			
			System.out.println( "Created pair manager tunnel: " + tunnel.getString());

		}
		
		return( true );
	}
	
	protected void
	closeTunnel(
		PairManagerTunnel		tunnel )
	{
		
		System.out.println( "Destroyed pair manager tunnel: " + tunnel.getString());

		synchronized( tunnels ){
			
			tunnels.remove( tunnel.getKey());
		}
	}
	
	protected void 
	generateEvidence(
		IndentWriter writer )
	{
		writer.println( "Tunnel Handler" );
		
		writer.indent();
		
		writer.println( "started=" + started + ", active=" + active );
		
		if ( init_fail != null ){
			
			writer.println( "Init fail: " + init_fail );
		}
		
		long	now = SystemTime.getMonotonousTime();
		
		writer.println( "total local=" + total_local_servers );
		writer.println( "last local create=" + (last_local_server_create_time==0?"<never>":String.valueOf(now-last_local_server_create_time)));
		writer.println( "last local agree=" + (last_local_server_agree_time==0?"<never>":String.valueOf(now-last_local_server_agree_time)));

		writer.println( "total remote=" + total_servers );
		writer.println( "last remote create=" + (last_server_create_time==0?"<never>":String.valueOf(now-last_server_create_time)));
		writer.println( "last remote agree=" + (last_server_agree_time==0?"<never>":String.valueOf(now-last_server_agree_time)));

		synchronized( tunnels ){

			writer.println( "tunnels=" + tunnels.size());
			
			for ( PairManagerTunnel tunnel: tunnels.values()){
				
				writer.println( "    " + tunnel.getString());
			}
		}
		
		try{
			writer.println( "IPv4 punchers: " + nat_punchers_ipv4.size());
			
			for ( DHTNATPuncher p: nat_punchers_ipv4 ){
				
				writer.println( "    " + p.getStats());
			}
			
			writer.println( "IPv6 punchers: " + nat_punchers_ipv6.size());
			
			for ( DHTNATPuncher p: nat_punchers_ipv6 ){
				
				writer.println( "    " + p.getStats());
			}
		}finally{
			
			writer.exdent();
		}
	}
}
