/*
 * Created on Jun 8, 2010
 * 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.net.udp.uc.impl;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HostNameToIPResolver;

import com.aelitis.azureus.core.proxy.AEProxyFactory;
import com.aelitis.azureus.core.proxy.AEProxySelector;
import com.aelitis.azureus.core.proxy.AEProxySelectorFactory;
import com.aelitis.net.udp.uc.PRUDPPacket;
import com.aelitis.net.udp.uc.PRUDPPacketHandler;
import com.aelitis.net.udp.uc.PRUDPPacketHandlerException;
import com.aelitis.net.udp.uc.PRUDPPacketHandlerStats;
import com.aelitis.net.udp.uc.PRUDPPacketReceiver;
import com.aelitis.net.udp.uc.PRUDPPrimordialHandler;
import com.aelitis.net.udp.uc.PRUDPRequestHandler;

public class 
PRUDPPacketHandlerSocks 
	implements PRUDPPacketHandler, PRUDPPacketHandlerImpl.PacketTransformer
{
	private static String	socks_host;
	private static int		socks_port;
	private static String	socks_user;
	private static String	socks_password;
	
	static{
		COConfigurationManager.addAndFireParameterListeners(
			new String[]{
				"Proxy.Host",
				"Proxy.Port",
				"Proxy.Username",
				"Proxy.Password",
			}, 
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String parameter_name ) 
				{
				    socks_host 		= COConfigurationManager.getStringParameter("Proxy.Host").trim();
					socks_port 		= Integer.parseInt(COConfigurationManager.getStringParameter("Proxy.Port").trim());
					socks_user 		= COConfigurationManager.getStringParameter("Proxy.Username").trim();
					socks_password 	= COConfigurationManager.getStringParameter("Proxy.Password").trim();
							     
					if ( socks_user.equalsIgnoreCase("<none>")){
						socks_user = "";
					}
						      
				}
			});
	}
	
	final private InetSocketAddress		target;
	
	private Socket					control_socket;
	
	private InetSocketAddress		relay;
	
	private PRUDPPacketHandler		delegate;
	
	private byte[]	packet_out_header;
		
	protected
	PRUDPPacketHandlerSocks(
		InetSocketAddress		_target )
	
		throws PRUDPPacketHandlerException
	{
		target	= _target;
		
		boolean	ok = false;
	
		AEProxySelector	proxy_selector = AEProxySelectorFactory.getSelector();
		
		Proxy proxy = proxy_selector.getSOCKSProxy( socks_host, socks_port, target );
		
		boolean		proxy_connected = false;
		Throwable	error			= null;
		
		try{
		    delegate = new PRUDPPacketHandlerImpl( 0, null, this );

			control_socket = new Socket( Proxy.NO_PROXY );
			
			InetSocketAddress proxy_address = (InetSocketAddress)proxy.address();
			
			control_socket.connect( proxy_address );
		
			proxy_connected	= true;
			
			DataOutputStream 	dos = new DataOutputStream( new BufferedOutputStream( control_socket.getOutputStream(), 256 ));
			DataInputStream 	dis = new DataInputStream( control_socket.getInputStream());
						
			dos.writeByte( (byte)5 ); // socks 5
			dos.writeByte( (byte)2 ); // 2 methods
			dos.writeByte( (byte)0 ); // no auth
			dos.writeByte( (byte)2 ); // user/pw
			
			dos.flush();
			
		    dis.readByte();  // version byte
		    
		    byte method = dis.readByte();

		    if ( method != 0 && method != 2 ){
		    	
		        throw new IOException( "SOCKS 5: no valid method [" + method + "]" );
		    }

		      // auth
		    
		    if ( method == 2 ) {
		        
		    	dos.writeByte( (byte)1 ); // user/pw version
		    	dos.writeByte( (byte)socks_user.length() ); // user length
		    	dos.write( socks_user.getBytes() );
		    	dos.writeByte( (byte)socks_password.length() ); // password length
		    	dos.write( socks_password.getBytes() );

		    	dos.flush();
		    	
		    	dis.readByte();  // version byte
		    	
		    	byte status = dis.readByte();

		        if ( status != 0 ){
		        	
		        	throw( new IOException( "SOCKS 5: authentication fails [status=" +status+ "]" ));
		        }
		    }
		    
		    String	mapped_ip;
		    
		    if ( target.isUnresolved() || target.getAddress() == null ){
		    	
		    		// deal with long "hostnames" that we get for, e.g., I2P destinations
		    	
		      	mapped_ip = AEProxyFactory.getAddressMapper().internalise( target.getHostName() );
		      	
		    }else{
		    	
		      	mapped_ip = AddressUtils.getHostNameNoResolve( target );
		    }
		    
		    dos.writeByte( (byte)5 ); // version
		    dos.writeByte( (byte)3 ); // udp associate
		    dos.writeByte( (byte)0 ); // reserved
		    
	    	dos.writeByte((byte)1);
	    	dos.write( new byte[4] );
	    	
		    dos.writeShort( (short)delegate.getPort()); // port
		    
		    dos.flush();
		    
		    dis.readByte();	// ver
		    
		    byte reply = dis.readByte();
		    
		    if ( reply != 0 ){
		    	
		    		// special hack for internal socks servers just being used for plumbing connections
		    		// for other protocols - 0x45 means 'go transparent'
		    	
		    	if ( reply == 0x45 && proxy_address.getAddress().isLoopbackAddress()){
		    		
		    		control_socket.close();
		    		
		    		control_socket = null;
		    		
		    		ok = true;
		    		
		    			// relay is null here - this drives other direct behaviour
		    		
		    		return;
		    	}
		    	
	        	throw( new IOException( "SOCKS 5: udp association fails [reply=" +reply+ "]" ));
		    }
		    
		    dis.readByte();	// reserved
		    
		    InetAddress	relay_address;
		    
		    byte atype = dis.readByte();
		    
		    if ( atype == 1 ){
		    	
		    	byte[]	bytes = new byte[4];
		    	
		    	dis.readFully( bytes );
		    	
		    	relay_address = InetAddress.getByAddress( bytes );
		    	
		    }else if ( atype == 3 ){
		    	
		    	byte	len = dis.readByte();
		    	
		    	byte[] bytes = new byte[(int)len&0xff ];
		    	
		    	dis.readFully( bytes );
		    	
		    	relay_address = InetAddress.getByName( new String( bytes ));
		    	
		    }else{
		    	
		    	byte[]	bytes = new byte[16];
		    	
		    	dis.readFully( bytes );
		    	
		    	relay_address = InetAddress.getByAddress( bytes );

		    }
		    
		    int	relay_port = ((dis.readByte()<<8)&0xff00) | (dis.readByte() & 0x00ff );
		    	
		    if ( relay_address.isAnyLocalAddress()){
		    	
		    	relay_address = control_socket.getInetAddress();
		    }
		    
		    relay = new InetSocketAddress( relay_address, relay_port );
		    			    
		    	// use the maped ip for dns resolution so we don't leak the
		    	// actual address if this is a secure one (e.g. I2P one)
		    
		    ByteArrayOutputStream	baos_temp 		= new ByteArrayOutputStream();
		    DataOutputStream		dos_temp	= new DataOutputStream( baos_temp );
	
		    dos_temp.writeByte(0);	// resv
		    dos_temp.writeByte(0);	// resv		    
		    dos_temp.writeByte(0);	// frag (none)
	
		    try {
		    	byte[] ip_bytes = HostNameToIPResolver.syncResolve( mapped_ip ).getAddress();
	
		    	dos_temp.writeByte( ip_bytes.length==4?(byte)1:(byte)4 );
		    	dos_temp.write( ip_bytes );
	
		    	
		    }catch( Throwable e ){
		    			    	
		    	dos_temp.writeByte( (byte)3 );  // address type = domain name
		    	dos_temp.writeByte( (byte)mapped_ip.length() );  // address type = domain name
		    	dos_temp.write( mapped_ip.getBytes() );
	
		    }
	
		    dos_temp.writeShort( (short)target.getPort() ); // port
	
		    dos_temp.flush();
		    packet_out_header = baos_temp.toByteArray();

		    	    
			ok = true;
			
			Thread.sleep(1000);
			
		}catch( Throwable e ){
			
			error = e;
			
			throw( new PRUDPPacketHandlerException( "socks setup failed: " + Debug.getNestedExceptionMessage(e), e));
			
		}finally{
			
			if ( !proxy_connected ){
				
				proxy_selector.connectFailed( proxy, error );
			}
			
			if ( !ok ){
				
				try{
					control_socket.close();
					
				}catch( Throwable e ){
					
					Debug.out( e );
					
				}finally{
					
					control_socket = null;
				}
				
				if ( delegate != null ){
					
					try{
					    delegate.destroy();
					    
					}finally{
						
						delegate = null;
					}
				}
			}
		}
	}
	
	public void
	transformSend(
		DatagramPacket	packet )
	{	
		if ( relay == null ){
			
			return;
		}
		
		byte[]	data 		= packet.getData();
		int		data_len	= packet.getLength();
		
		byte[]	new_data = new byte[data_len+packet_out_header.length];
		
		System.arraycopy( packet_out_header, 0, new_data, 0, packet_out_header.length );
		System.arraycopy( data, 0, new_data, packet_out_header.length, data_len);
		
		packet.setData( new_data );
	}
		
	public void
	transformReceive(
		DatagramPacket	packet )
	{
		if ( relay == null ){
			
			return;
		}
		
		byte[]	data 		= packet.getData();
		int		data_len	= packet.getLength();
		
		DataInputStream dis = new DataInputStream( new ByteArrayInputStream( data, 0, data_len ));
		
		try{
			dis.readByte();	// res
			dis.readByte();	// res
			dis.readByte();	// assume no frag
		
			byte	atype = dis.readByte();
			
			int	encap_len = 4;
			if ( atype == 1 ){
				
				encap_len += 4;
				
			}else if ( atype == 3 ){
				
				encap_len += 1 + (dis.readByte()&0xff);
				
			}else{
				
				encap_len += 16;
			}
			
			encap_len += 2;	// port
			
			byte[]	new_data = new byte[data_len-encap_len];
			
			System.arraycopy( data, encap_len, new_data, 0, data_len - encap_len );
			
			packet.setData( new_data );
			
		}catch( IOException e ){
			
			Debug.out( e );
		}
	}
			
	private void
	checkAddress(
		InetSocketAddress			destination )
	
		throws PRUDPPacketHandlerException
	{
		if ( !destination.equals( target )){
			
			throw( new PRUDPPacketHandlerException( "Destination mismatch" ));
		}
	}
	
	public void
	sendAndReceive(
		PRUDPPacket					request_packet,
		InetSocketAddress			destination_address,
		PRUDPPacketReceiver			receiver,
		long						timeout,
		int							priority )
	
		throws PRUDPPacketHandlerException
	{
		checkAddress( destination_address );
		
		if ( relay != null ){
			
			destination_address = relay;
		}
		
		delegate.sendAndReceive( request_packet, destination_address, receiver, timeout, priority );
	}
	
	public PRUDPPacket
	sendAndReceive(
		PasswordAuthentication		auth,
		PRUDPPacket					request_packet,
		InetSocketAddress			destination_address )
	
		throws PRUDPPacketHandlerException
	{
		checkAddress( destination_address );
		
		if ( relay != null ){
			
			destination_address = relay;
		}
		
		return( delegate.sendAndReceive( auth, request_packet, destination_address));
	}
	
	public PRUDPPacket
	sendAndReceive(
		PasswordAuthentication		auth,
		PRUDPPacket					request_packet,
		InetSocketAddress			destination_address,
		long						timeout_millis )
	
		throws PRUDPPacketHandlerException
	{
		checkAddress( destination_address );
		
		if ( relay != null ){
			
			destination_address = relay;
		}
		
		return( delegate.sendAndReceive(auth, request_packet, destination_address, timeout_millis ));
	}
	
	public PRUDPPacket
	sendAndReceive(
		PasswordAuthentication		auth,
		PRUDPPacket					request_packet,
		InetSocketAddress			destination_address,
		long						timeout_millis,
		int							priority )
	
		throws PRUDPPacketHandlerException
	{
		checkAddress( destination_address );
		
		if ( relay != null ){
			
			destination_address = relay;
		}
		
		return( delegate.sendAndReceive(auth, request_packet, destination_address, timeout_millis, priority ));
	}
	
	public void
	send(
		PRUDPPacket					request_packet,
		InetSocketAddress			destination_address )
	
		throws PRUDPPacketHandlerException
	{
		checkAddress( destination_address );
		
		if ( relay != null ){
			
			destination_address = relay;
		}
		
		delegate.send( request_packet, destination_address );
	}
	
	public PRUDPRequestHandler
	getRequestHandler()
	{
		return( delegate.getRequestHandler());
	}
	
	public void
	setRequestHandler(
		PRUDPRequestHandler	request_handler )
	{
		delegate.setRequestHandler( request_handler );
	}
	
	public void
	primordialSend(
		byte[]				data,
		InetSocketAddress	target )
	
		throws PRUDPPacketHandlerException
	{
		throw( new PRUDPPacketHandlerException( "not imp" ));
	}
	
	public boolean
	hasPrimordialHandler()
	{
		return( delegate.hasPrimordialHandler());
	}
	
	public void
	addPrimordialHandler(
		PRUDPPrimordialHandler	handler )
	{
	}
	
	public void
	removePrimordialHandler(
		PRUDPPrimordialHandler	handler )
	{
	}
	
	public int
	getPort()
	{
		return( delegate.getPort());
	}
	
	public InetAddress 
	getBindIP() 
	{
		return( delegate.getBindIP());
	}
	
	public void
	setDelays(
		int		send_delay,
		int		receive_delay,
		int		queued_request_timeout )
	{
		delegate.setDelays(send_delay, receive_delay, queued_request_timeout);
	}
	
	public void
	setExplicitBindAddress(
		InetAddress	address )
	{
		delegate.setExplicitBindAddress( address );
	}
	
	public PRUDPPacketHandlerStats
	getStats()
	{
		return( delegate.getStats());
	}
	
	public PRUDPPacketHandler
	openSession(
		InetSocketAddress	target )
	
		throws PRUDPPacketHandlerException
	{
		throw( new PRUDPPacketHandlerException( "not supported" ));
	}
	
	public void
	closeSession()
	
		throws PRUDPPacketHandlerException
	{	
		if ( control_socket != null ){
			
			try{
				control_socket.close();
				
				control_socket = null;
				
			}catch( Throwable e ){
				
				Debug.out( e );
			}
		}
		
		if ( delegate != null ){
			
			delegate.destroy();
		}
	}
	
	public void
	destroy()
	{
		try{
			closeSession();
			
		}catch( Throwable e ){
			
			Debug.out( e );
		}
	}
}
