/*
 * Created on Dec 20, 2004
 * Created by Alon Rohter
 * 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.versioncheck;


import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.util.*;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.download.impl.DownloadManagerStateImpl;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.stats.transfer.*;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.utils.DelayedTask;
import org.gudy.azureus2.pluginsimpl.local.utils.UtilitiesImpl;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.impl.AzureusCoreImpl;
import com.aelitis.azureus.core.clientmessageservice.*;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminASN;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.proxy.AEProxyFactory;
import com.aelitis.azureus.core.proxy.AEProxyFactory.PluginProxy;
import com.aelitis.azureus.core.security.CryptoManagerFactory;
import com.aelitis.azureus.core.util.DNSUtils;
import com.aelitis.net.udp.uc.PRUDPPacketHandler;
import com.aelitis.net.udp.uc.PRUDPPacketHandlerFactory;
import com.aelitis.net.udp.uc.PRUDPReleasablePacketHandler;



/**
 * Client for checking version information from a remote server.
 */
public class VersionCheckClient {
	private static final LogIDs LOGID = LogIDs.CORE;

	public static final String	REASON_UPDATE_CHECK_START		= "us";
	public static final String	REASON_UPDATE_CHECK_PERIODIC	= "up";
	public static final String	REASON_CHECK_SWT				= "sw";
	public static final String	REASON_DHT_FLAGS				= "df";
	public static final String	REASON_DHT_EXTENDED_ALLOWED		= "dx";
	public static final String	REASON_DHT_ENABLE_ALLOWED		= "de";
	public static final String	REASON_EXTERNAL_IP				= "ip";
	public static final String	REASON_RECOMMENDED_PLUGINS		= "rp";
	public static final String	REASON_SECONDARY_CHECK			= "sc";
	public static final String	REASON_PLUGIN_UPDATE			= "pu";
	public static final String	REASON_DHT_BOOTSTRAP			= "db";


	private static final String 	AZ_MSG_SERVER_ADDRESS_V4 	= Constants.VERSION_SERVER_V4;
	private static final int 		AZ_MSG_SERVER_PORT 			= 27001;
	private static final String 	MESSAGE_TYPE_ID 			= "AZVER";

	public static final String 		HTTP_SERVER_ADDRESS_V4		= AZ_MSG_SERVER_ADDRESS_V4;
	public static final int 		HTTP_SERVER_PORT 			= 80;

	public static final String 		TCP_SERVER_ADDRESS_V4		= AZ_MSG_SERVER_ADDRESS_V4;
	public static final int 		TCP_SERVER_PORT 			= 80;

	public static final String 		UDP_SERVER_ADDRESS_V4		= AZ_MSG_SERVER_ADDRESS_V4;
	public static final int 		UDP_SERVER_PORT 			= 2080;

	public static final String		AZ_MSG_SERVER_ADDRESS_V6 	= Constants.VERSION_SERVER_V6;
	public static final String		HTTP_SERVER_ADDRESS_V6 		= AZ_MSG_SERVER_ADDRESS_V6;
	public static final String		TCP_SERVER_ADDRESS_V6 		= AZ_MSG_SERVER_ADDRESS_V6;
	public static final String		UDP_SERVER_ADDRESS_V6 		= AZ_MSG_SERVER_ADDRESS_V6;


	private static final long		CACHE_PERIOD	= 5*60*1000;
	private static boolean secondary_check_done;

	private final List<VersionCheckClientListener> listeners = new ArrayList<VersionCheckClientListener>(1);
	private boolean startCheckRan = false;

	static{
		VersionCheckClientUDPCodecs.registerCodecs();
	}

	private static final int	AT_V4		= 1;
	private static final int	AT_V6		= 2;
	private static final int	AT_EITHER	= 3;

	private static VersionCheckClient instance;

	/**
	 * Get the singleton instance of the version check client.
	 * @return version check client
	 */
	public static synchronized VersionCheckClient 
	getSingleton()
	{
		if ( instance == null ){

			instance = new VersionCheckClient();
		}

		return( instance );
	}

	private boolean	enable_v6;
	private boolean	prefer_v6;

	private Map last_check_data_v4 = null;
	private Map last_check_data_v6 = null;

	private final AEMonitor check_mon = new AEMonitor( "versioncheckclient" );

	private long last_check_time_v4 = 0; 
	private long last_check_time_v6 = 0; 

	private long last_feature_flag_cache;
	private long last_feature_flag_cache_time;


	private 
	VersionCheckClient()
	{
		COConfigurationManager.addAndFireParameterListeners(
				new String[]{ "IPV6 Prefer Addresses", "IPV6 Enable Support" },
				new ParameterListener()
				{
					public void 
					parameterChanged(
							String 	name )
					{
						enable_v6	= COConfigurationManager.getBooleanParameter( "IPV6 Enable Support" );
						prefer_v6 	= COConfigurationManager.getBooleanParameter( "IPV6 Prefer Addresses" );
					} 
				});
	}

	public void
	initialise()
	{
		DelayedTask delayed_task = 
			UtilitiesImpl.addDelayedTask(
					"VersionCheck", 
					new Runnable()
					{
						public void
						run()
						{
							final AESemaphore sem = new AESemaphore( "VCC:init" );

							new AEThread2( "VCC:init", true )
							{
								public void 
								run()
								{
									try{
										getVersionCheckInfo( REASON_UPDATE_CHECK_START );

									}finally{

										sem.release();
									}
								}
							}.start();

							if ( !sem.reserve( 5000 )){

								Debug.out( "Timeout waiting for version check to complete" );
							}
						}
					});

		delayed_task.queue();
	}






	/**
	 * Get the version check reply info.
	 * @return reply data, possibly cached, if the server was already checked within the last minute
	 */

	public Map 
	getVersionCheckInfo( 
			String 	reason )
	{
		return( getVersionCheckInfo( reason, AT_EITHER ));
	}

	public Map 
	getVersionCheckInfo( 
			String 	reason,
			int		address_type )
	{
		if ( address_type == AT_V4 ){

			return( getVersionCheckInfoSupport( reason, false, false, false ));

		}else if ( address_type == AT_V6 ){

			return( getVersionCheckInfoSupport( reason, false, false, true ));

		}else{

			Map	reply = getVersionCheckInfoSupport( reason, false, false, prefer_v6 );

			if ( reply == null || reply.size() == 0 ){

				reply =  getVersionCheckInfoSupport( reason, false, false, !prefer_v6 );
			}

			return( reply );
		}
	}


	protected Map 
	getVersionCheckInfoSupport( 
		String 	reason, 
		boolean only_if_cached, 
		boolean force,
		boolean	v6 )
	{
		try {
			synchronized (listeners) {
				if (REASON_UPDATE_CHECK_START.equals(reason)) {
					startCheckRan = true;
				}
				for (VersionCheckClientListener l : listeners) {
					l.versionCheckStarted(reason);
				}
			}
		} catch (Throwable t) {
			Debug.out(t);
		}
		
		if ( v6 ){

			if ( enable_v6 ){

				try {  check_mon.enter();

				long time_diff = SystemTime.getCurrentTime() - last_check_time_v6;

				force = force || time_diff > CACHE_PERIOD || time_diff < 0;

				if( last_check_data_v6 == null || last_check_data_v6.size() == 0 || force ) {
					// if we've never checked before then we go ahead even if the "only_if_cached"
					// flag is set as its had not chance of being cached yet!
					if ( only_if_cached && last_check_data_v6 != null ){
						return( new HashMap() );
					}
					try {
						last_check_data_v6 = performVersionCheck( constructVersionCheckMessage( reason ), true, true, true );

						if ( last_check_data_v6 != null && last_check_data_v6.size() > 0 ){

							COConfigurationManager.setParameter( "versioncheck.cache.v6", last_check_data_v6 );
						}
					}
					catch(SocketException t) {
						// internet is broken
						// Debug.out(t.getClass().getName() + ": " + t.getMessage());
					}
					catch(UnknownHostException t) {
						// dns is broken
						// Debug.out(t.getClass().getName() + ": " + t.getMessage());
					}
					catch( Throwable t ) {
						Debug.out(t);
						last_check_data_v6 = new HashMap();
					}
				}
				else {
					Logger.log(new LogEvent(LOGID, "VersionCheckClient is using "
							+ "cached version check info. Using " + last_check_data_v6.size()
							+ " reply keys.")); 
				}
				}
				finally {  check_mon.exit();  }
			}

			if( last_check_data_v6 == null )  last_check_data_v6 = new HashMap();

			return last_check_data_v6;

		}else{

			try {  check_mon.enter();

			long time_diff = SystemTime.getCurrentTime() - last_check_time_v4;

			force = force || time_diff > CACHE_PERIOD || time_diff < 0;

			if( last_check_data_v4 == null || last_check_data_v4.size() == 0 || force ) {
				// if we've never checked before then we go ahead even if the "only_if_cached"
				// flag is set as its had not chance of being cached yet!
				if ( only_if_cached && last_check_data_v4 != null ){
					return( new HashMap() );
				}
				try {
					last_check_data_v4 = performVersionCheck( constructVersionCheckMessage( reason ), true, true, false );

					if ( last_check_data_v4 != null && last_check_data_v4.size() > 0 ){

						COConfigurationManager.setParameter( "versioncheck.cache.v4", last_check_data_v4 );
					}

					// clear down any plugin-specific data that has successfully been sent to the version server

					try{
						if ( AzureusCoreFactory.isCoreAvailable() && AzureusCoreFactory.getSingleton().getPluginManager().isInitialized()){

							//installed plugin IDs
							PluginInterface[] plugins = AzureusCoreFactory.getSingleton().getPluginManager().getPluginInterfaces();

							for (int i=0;i<plugins.length;i++){

								PluginInterface		plugin = plugins[i];

								Map	data = plugin.getPluginconfig().getPluginMapParameter( "plugin.versionserver.data", null );

								if ( data != null ){

									plugin.getPluginconfig().setPluginMapParameter( "plugin.versionserver.data", new HashMap());
								}
							}
						}
					}catch( Throwable e ){
					}
				}
				catch( UnknownHostException t ) {
					// no internet
					Debug.outNoStack("VersionCheckClient - " + t.getClass().getName() + ": " + t.getMessage());
				}
				catch (IOException t) {
					// General connection problem.
					Debug.outNoStack("VersionCheckClient - " + t.getClass().getName() + ": " + t.getMessage());
				}
				catch( Throwable t ) {
					Debug.out(t);
					last_check_data_v4 = new HashMap();
				}
			}
			else {
				if (Logger.isEnabled())
					Logger.log(new LogEvent(LOGID, "VersionCheckClient is using "
							+ "cached version check info. Using " + last_check_data_v4.size()
							+ " reply keys.")); 
			}
			}
			finally {  check_mon.exit();  }

			if( last_check_data_v4 == null )  last_check_data_v4 = new HashMap();

			last_feature_flag_cache_time = 0;

			return last_check_data_v4;
		}
	}

	public Map
	getMostRecentVersionCheckData()
	{
		// currently we maintain v4 much more accurately than v6

		if ( last_check_data_v4 != null ){

			return( last_check_data_v4 );
		}

		Map	res = COConfigurationManager.getMapParameter( "versioncheck.cache.v4", null );

		if ( res != null ){

			return( res );
		}

		if ( last_check_data_v6 != null ){

			return( last_check_data_v6 );
		}

		res = COConfigurationManager.getMapParameter( "versioncheck.cache.v6", null );

		return( res );
	}

	private boolean
	isVersionCheckDataValid(
		int		address_type )
	{  	
		boolean v6_ok = last_check_data_v6 != null && last_check_data_v6.size() > 0;
		boolean v4_ok = last_check_data_v4 != null && last_check_data_v4.size() > 0;

		if ( address_type == AT_V4 ){

			return( v4_ok );

		}else if ( address_type == AT_V6 ){

			return( v6_ok );

		}else{

			return( v4_ok | v6_ok );
		}
	}

	public long
	getCacheTime(
			boolean v6 )
	{
		return( v6?last_check_time_v6:last_check_time_v4);
	}

	public void
	clearCache()
	{
		last_check_time_v6	= 0;
		last_check_time_v4	= 0;
	}

	public long
	getFeatureFlags()
	{
		long	now = SystemTime.getCurrentTime();

		if ( now > last_feature_flag_cache_time && now - last_feature_flag_cache_time < 60000 ){

			return( last_feature_flag_cache );
		}

		Map m = getMostRecentVersionCheckData();

		long	result;

		if ( m == null ){

			result = 0;

		}else{

			byte[]	b_feat_flags = (byte[])m.get( "feat_flags" );

			if ( b_feat_flags != null ){

				try{

					result = Long.parseLong(new String((byte[])b_feat_flags));

				}catch( Throwable e ){

					result	= 0;
				}
			}else{

				result = 0;
			}
		}

		last_feature_flag_cache 		= result;
		last_feature_flag_cache_time	= now;

		return( result );
	}

	public Set<String>
	getDisabledPluginIDs()
	{  
		Set<String>	result = new HashSet<String>();

		Map m = getMostRecentVersionCheckData();

		if ( m != null ){

			byte[]	x = (byte[])m.get( "disabled_pids" );

			if ( x != null ){

				String str = new String( x );

				String latest = COConfigurationManager.getStringParameter( "vc.disabled_pids.latest", "" );

				if ( !str.equals( latest )){

					byte[]	sig = (byte[])m.get( "disabled_pids_sig" );

					if ( sig == null ){

						Debug.out( "disabled plugins sig missing" );

						return( result );
					}

					try{
						AEVerifier.verifyData( str, sig );

						COConfigurationManager.setParameter( "vc.disabled_pids.latest", str );

					}catch( Throwable e ){

						return( result );
					}
				}

				String[] bits = str.split( "," );

				for ( String b: bits ){

					b = b.trim();

					if ( b.length() > 0 ){

						result.add( b );
					}
				}
			}
		}

		return( result );
	}

	public Set<String>
	getAutoInstallPluginIDs()
	{  
		Set<String>	result = new HashSet<String>();

		Map m = getMostRecentVersionCheckData();

		if ( m != null ){

			byte[]	x = (byte[])m.get( "autoinstall_pids" );

			if ( x != null ){

				String str = new String( x );

				String latest = COConfigurationManager.getStringParameter( "vc.autoinstall_pids.latest", "" );

				if ( !str.equals( latest )){

					byte[]	sig = (byte[])m.get( "autoinstall_pids_sig" );

					if ( sig == null ){

						Debug.out( "autoinstall plugins sig missing" );

						return( result );
					}

					try{
						AEVerifier.verifyData( str, sig );

						COConfigurationManager.setParameter( "vc.autoinstall_pids.latest", str );

					}catch( Throwable e ){

						return( result );
					}
				}

				String[] bits = str.split( "," );

				for ( String b: bits ){

					b = b.trim();

					if ( b.length() > 0 ){

						result.add( b );
					}
				}
			}
		}

		return( result );
	}

	/**
	 * Get the ip address seen by the version check server.
	 * NOTE: This information may be cached, see getVersionCheckInfo().
	 * @return external ip address, or empty string if no address information found
	 */

	public String 
	getExternalIpAddress(
			boolean	only_if_cached,
			boolean	v6 )
	{
		return( getExternalIpAddress( only_if_cached, v6, false ));
	}
	
	public String 
	getExternalIpAddress(
		boolean		only_if_cached,
		boolean		v6,
		boolean		force )
	{
		Map reply = getVersionCheckInfoSupport( REASON_EXTERNAL_IP, only_if_cached, force, v6 );

		byte[] address = (byte[])reply.get( "source_ip_address" );
		if( address != null ) {
			return new String( address );
		}

		return( null );
	}


	/**
	 * Is the DHT plugin allowed to be enabled.
	 * @return true if DHT can be enabled, false if it should not be enabled
	 */
	public boolean DHTEnableAllowed() {
		Map reply = getVersionCheckInfo( REASON_DHT_ENABLE_ALLOWED, AT_EITHER );

		boolean	res = false;

		byte[] value = (byte[])reply.get( "enable_dht" );

		if( value != null ) {

			res = new String( value ).equalsIgnoreCase( "true" );
		}

		// we take the view that if the version check failed then we go ahead
		// and enable the DHT (i.e. we're being optimistic)

		if ( !res ){
			res = !isVersionCheckDataValid( AT_EITHER );
		}

		return res;
	}


	/**
	 * Is the DHT allowed to be used by external plugins.
	 * @return true if extended DHT use is allowed, false if not allowed
	 */
	public boolean 
	DHTExtendedUseAllowed() 
	{
		Map reply = getVersionCheckInfo( REASON_DHT_EXTENDED_ALLOWED, AT_EITHER );

		boolean	res = false;

		byte[] value = (byte[])reply.get( "enable_dht_extended_use" );
		if( value != null ) {
			res = new String( value ).equalsIgnoreCase( "true" );
		}

		// be generous and enable extended use if check failed

		if ( !res ){
			res = !isVersionCheckDataValid( AT_EITHER );
		}

		return res;
	}

	public byte 
	getDHTFlags() 
	{
		Map map = getMostRecentVersionCheckData();

		if ( map != null ){
			
			byte[] b_flags = (byte[])map.get( "dht_flags" );
			
			if ( b_flags != null ){
				
				return( new Integer(new String( b_flags )).byteValue());
			}
		}
		
		return( (byte)0xff );
	}
	
	public String[]
	getRecommendedPlugins()
	{
		Map reply = getVersionCheckInfo( REASON_RECOMMENDED_PLUGINS, AT_EITHER );

		List	l = (List)reply.get( "recommended_plugins" );

		if ( l == null ){

			return( new String[0] );
		}

		String[]	res = new String[l.size()];

		for (int i=0;i<l.size();i++){

			res[i] = new String((byte[])l.get(i));
		}

		return( res );
	}

	public List<InetSocketAddress>
	getDHTBootstrap(
		boolean 	ipv4 )
	{
		List<InetSocketAddress>	result = new ArrayList<InetSocketAddress>();

		try{
			Map reply = getVersionCheckInfo( REASON_DHT_BOOTSTRAP, ipv4?AT_V4:AT_V6 );
	
			List<Map>	l = (List<Map>)reply.get( "dht_boot" );
	
			if ( l != null ){
	
				for( Map m: l ){
					
					byte[]	address = (byte[])m.get("a");
					int		port	= ((Long)m.get("p")).intValue();
					
					if (	ipv4 && address.length == 4 ||
							!ipv4 && address.length == 16 ){
						
						InetAddress iaddress = InetAddress.getByAddress( address );
						
						if ( !(	iaddress.isLoopbackAddress() ||
								iaddress.isLinkLocalAddress() ||
								iaddress.isSiteLocalAddress())){
				
							result.add( new InetSocketAddress( iaddress, port ));
						}
					}
				}
			}
		}catch( Throwable e ){
			
			Debug.out( e );
		}
		
		return( result );
	}

	public Map<String,Object>
	getCountryInfo()
	{
		Map reply = getVersionCheckInfo( REASON_EXTERNAL_IP, AT_EITHER );

		Map<String,Object> info = (Map<String,Object>)reply.get( "source_info" );

		if ( info == null ){

			return( new HashMap<String,Object>());

		}else{

			return( BDecoder.decodeStrings( info ));
		}
	}

	/**
	 * Perform the actual version check by connecting to the version server.
	 * @param data_to_send version message
	 * @return version reply
	 * @throws Exception if the server check connection fails
	 */
	private Map 
	performVersionCheck( 
		Map 	data_to_send,
		boolean	use_az_message,
		boolean	use_http,
		boolean	v6 )

		throws Exception 
	{
		Exception 	error 	= null;
		Map			reply	= null;


		if ( use_http ){

			try{
				reply = executeHTTP( data_to_send, v6 );

				reply.put( "protocol_used", "HTTP" );

				error = null;
			}
			catch (IOException e) {
				error = e;
			}
			catch (Exception e){
				Debug.printStackTrace(e);
				error = e;

			}
		}

		if ( reply == null && use_az_message ){

			try{
				reply = executeAZMessage( data_to_send, v6 );

				reply.put( "protocol_used", "AZMSG" );
				
				error = null;
			}
			catch (IOException e) {
				error = e;
			}
			catch (Exception e) {
				Debug.printStackTrace( e );
				error = e;
			}
		}
		if ( error != null ){

			throw( error );
		}

		if (Logger.isEnabled())
			Logger.log(new LogEvent(LOGID, "VersionCheckClient server "
					+ "version check successful. Received " + (reply==null?"null":reply.size())
					+ " reply keys."));

		if ( v6 ){

			last_check_time_v6 = SystemTime.getCurrentTime();

		}else{

			last_check_time_v4 = SystemTime.getCurrentTime();
		}

		return reply;
	}

	private Map
	executeAZMessage(
		Map		data_to_send,
		boolean	v6 )

		throws Exception
	{
		if ( v6 && !enable_v6 ){

			throw( new Exception( "IPv6 is disabled" ));
		}

		String	host = getHost( v6, AZ_MSG_SERVER_ADDRESS_V6, AZ_MSG_SERVER_ADDRESS_V4 );

		if (Logger.isEnabled())
			Logger.log(new LogEvent(LOGID, "VersionCheckClient retrieving "
					+ "version information from " + host + ":" + AZ_MSG_SERVER_PORT)); 

		ClientMessageService 	msg_service = null;
		Map 					reply		= null;	

		try{
			msg_service = ClientMessageServiceClient.getServerService( host, AZ_MSG_SERVER_PORT, 20, MESSAGE_TYPE_ID );

			msg_service.sendMessage( data_to_send );  //send our version message

			reply = msg_service.receiveMessage();  //get the server reply

			preProcessReply( reply, v6 );

		}finally{

			if ( msg_service != null ){

				msg_service.close();
			}
		}

		return( reply );
	}

	private Map
	executeHTTP(
		Map		data_to_send,
		boolean	v6 )

		throws Exception
	{
		if ( v6 && !enable_v6 ){

			throw( new Exception( "IPv6 is disabled" ));
		}

		String	host = getHost(v6, HTTP_SERVER_ADDRESS_V6, HTTP_SERVER_ADDRESS_V4 );

		if (Logger.isEnabled())
			Logger.log(new LogEvent(LOGID, "VersionCheckClient retrieving "
					+ "version information from " + host + ":" + HTTP_SERVER_PORT + " via HTTP" )); 

		String	url_str = "http://" + (v6?UrlUtils.convertIPV6Host(host):host) + (HTTP_SERVER_PORT==80?"":(":" + HTTP_SERVER_PORT)) + "/version?";

		url_str += URLEncoder.encode( new String( BEncoder.encode( data_to_send ), "ISO-8859-1" ), "ISO-8859-1" );

		URL	url = new URL( url_str );

		try{
			HttpURLConnection	url_connection = (HttpURLConnection)url.openConnection();
				
			url_connection.setConnectTimeout( 10*1000 );
			url_connection.setReadTimeout( 10*1000 );
	
			url_connection.connect();
	
			try{
				InputStream	is = url_connection.getInputStream();
	
				Map	reply = BDecoder.decode( new BufferedInputStream( is ));
	
				preProcessReply( reply, v6 );
	
				return( reply );
	
			}finally{
	
				url_connection.disconnect();
			}
		}catch( Exception e ){
			
			if ( !v6 ){
				
				PluginProxy proxy = AEProxyFactory.getPluginProxy( "Vuze version check", url );
				
				if ( proxy != null ){
					
					boolean	worked = false;
					
					try{
						HttpURLConnection url_connection = (HttpURLConnection)proxy.getURL().openConnection( proxy.getProxy());
						
						url_connection.setConnectTimeout( 30*1000 );
						url_connection.setReadTimeout( 30*1000 );
				
						url_connection.connect();
				
						try{
							InputStream	is = url_connection.getInputStream();
				
							Map	reply = BDecoder.decode( new BufferedInputStream( is ));
				
							preProcessReply( reply, v6 );
				
							worked = true;
							
							return( reply );
				
						}finally{
				
							url_connection.disconnect();
						}
					}finally{
						
						proxy.setOK( worked );
					}
				}
			}
			
			throw( e );
		}
	}

	public String
	getHTTPGetString(
		boolean	for_proxy,
		boolean	v6 )
	{
		return( getHTTPGetString( new HashMap(), for_proxy, v6 ));
	}

	private String
	getHTTPGetString(
		Map		content,
		boolean	for_proxy,
		boolean	v6 )
	{
		String	host = getHost( v6, HTTP_SERVER_ADDRESS_V6, HTTP_SERVER_ADDRESS_V4 );

		String	get_str = "GET " + (for_proxy?("http://" + (v6?UrlUtils.convertIPV6Host( host ):host ) + ":" + HTTP_SERVER_PORT ):"") +"/version?";

		try{
			get_str += URLEncoder.encode( new String( BEncoder.encode( content ), "ISO-8859-1" ), "ISO-8859-1" );

		}catch( Throwable e ){ 
		}

		get_str +=" HTTP/1.1" + "\015\012" + "\015\012";

		return( get_str );
	}

	private Map
	executeTCP(
		Map				data_to_send,
		InetAddress		bind_ip,
		int				bind_port,
		boolean			v6 )

		throws Exception
	{
		if ( v6 && !enable_v6 ){

			throw( new Exception( "IPv6 is disabled" ));
		}

		String	host = getHost(v6, TCP_SERVER_ADDRESS_V6, TCP_SERVER_ADDRESS_V4 );

		if (Logger.isEnabled())
			Logger.log(new LogEvent(LOGID, "VersionCheckClient retrieving "
					+ "version information from " + host + ":" + TCP_SERVER_PORT + " via TCP" )); 

		String	get_str = getHTTPGetString( data_to_send, false, v6 );

		Socket	socket = null;

		try{
			socket = new Socket();

			if ( bind_ip != null ){

				socket.bind( new InetSocketAddress( bind_ip, bind_port ));

			}else if ( bind_port != 0 ){

				socket.bind( new InetSocketAddress( bind_port ));
			}

			socket.setSoTimeout( 10000 );

			socket.connect( new InetSocketAddress( host, TCP_SERVER_PORT ), 10000 );

			OutputStream	os = socket.getOutputStream();

			os.write( get_str.getBytes( "ISO-8859-1" ));

			os.flush();

			InputStream	is = socket.getInputStream();

			byte[]	buffer = new byte[1];

			String	header = "";

			int content_length = -1;

			while( true ){

				int	len = is.read( buffer );

				if ( len <= 0 ){

					break;
				}

				header += (char)buffer[0];

				if ( header.endsWith( "\r\n\r\n" )){

					header = header.toLowerCase( MessageText.LOCALE_ENGLISH );

					int	pos = header.indexOf( "content-length:" );

					if ( pos == -1 ){

						throw( new IOException( "content length missing" ));
					}

					header = header.substring( pos+15 );

					pos = header.indexOf( '\r' );

					header = header.substring(0,pos).trim();

					content_length = Integer.parseInt( header );

					if ( content_length > 10000 ){

						throw( new IOException( "content length too large" ));
					}

					break;
				}

				if ( header.length() > 2048 ){

					throw( new IOException( "header too large" ));
				}
			}

			ByteArrayOutputStream	baos = new ByteArrayOutputStream( content_length );

			buffer = new byte[content_length];

			while( content_length > 0 ){

				int	len = is.read( buffer );

				if ( len <= 0 ){

					break;
				}

				baos.write( buffer, 0, len );

				content_length -= len;
			}

			if ( content_length != 0 ){

				throw( new IOException( "error reading reply" ));
			}

			byte[]	reply_bytes = baos.toByteArray();

			Map reply = BDecoder.decode( new BufferedInputStream( new ByteArrayInputStream( reply_bytes )));

			preProcessReply( reply, v6 );

			return( reply );

		}finally{

			if ( socket != null ){

				try{
					socket.close();

				}catch( Throwable e ){

				}
			}
		}
	}

	private Map
	executeUDP(
		Map				data_to_send,
		InetAddress		bind_ip,
		int				bind_port,
		boolean			v6 )

		throws Exception
	{
		if ( v6 && !enable_v6 ){

			throw( new Exception( "IPv6 is disabled" ));
		}

		String	host = getHost( v6, UDP_SERVER_ADDRESS_V6, UDP_SERVER_ADDRESS_V4 );

		PRUDPReleasablePacketHandler handler = PRUDPPacketHandlerFactory.getReleasableHandler( bind_port );

		PRUDPPacketHandler	packet_handler = handler.getHandler();

		long timeout = 5;

		Random random = new Random();

		try{
			Exception	last_error = null;

			packet_handler.setExplicitBindAddress( bind_ip );	  

			for (int i=0;i<3;i++){

				try{
					// connection ids for requests must always have their msb set...
					// apart from the original darn udp tracker spec....

					long connection_id = 0x8000000000000000L | random.nextLong();

					VersionCheckClientUDPRequest	request_packet = new VersionCheckClientUDPRequest( connection_id );

					request_packet.setPayload( data_to_send );

					VersionCheckClientUDPReply reply_packet = (VersionCheckClientUDPReply)packet_handler.sendAndReceive( null, request_packet, new InetSocketAddress( host, UDP_SERVER_PORT ), timeout );

					Map	reply = reply_packet.getPayload();

					preProcessReply( reply, v6 );

					return( reply );

				}catch( Exception e){

					last_error	= e;

					timeout = timeout * 2;
				}
			}

			if ( last_error != null ){

				throw( last_error );
			}

			throw( new Exception( "Timeout" ));

		}finally{

			packet_handler.setExplicitBindAddress( null );

			handler.release();
		}
	}

	protected void
	preProcessReply(
		Map					reply,
		final boolean		v6 )
	{
		NetworkAdmin admin = NetworkAdmin.getSingleton();

		try{
			byte[] address = (byte[])reply.get( "source_ip_address" );

			if ( address != null ){
				
				InetAddress my_ip = InetAddress.getByName( new String( address ));
	
				NetworkAdminASN old_asn = admin.getCurrentASN();
	
				NetworkAdminASN new_asn = admin.lookupCurrentASN( my_ip );
	
				if ( !new_asn.sameAs( old_asn )){
	
					// kick off a secondary version check to communicate the new information
	
					if ( !secondary_check_done ){
	
						secondary_check_done	= true;
	
						new AEThread( "Secondary version check", true )
						{
							public void
							runSupport()
							{
								getVersionCheckInfoSupport( REASON_SECONDARY_CHECK, false, true, v6 );
							}
						}.start();
					}
				}
			}
		}catch( Throwable e ){

			if ( !Debug.containsException( e, UnknownHostException.class )){
			
				Debug.printStackTrace(e);
			}
		}

		Long	as_advice = (Long)reply.get( "as_advice" );

		if ( as_advice != null ){

			NetworkAdminASN current_asn = admin.getCurrentASN();

			String	asn = current_asn.getASName();

			if ( asn != null ){

				long	advice = as_advice.longValue();

				if ( advice != 0 ){

					// require crypto

					String	done_asn = COConfigurationManager.getStringParameter( "ASN Advice Followed", "" );

					if ( !done_asn.equals( asn )){

						COConfigurationManager.setParameter( "ASN Advice Followed", asn );

						boolean	change 	= advice == 1 || advice == 2;
						boolean	alert 	= advice == 1 || advice == 3;

						if ( !COConfigurationManager.getBooleanParameter( "network.transport.encrypted.require" )){

							if ( change ){

								COConfigurationManager.setParameter( "network.transport.encrypted.require", true );
							}

							if ( alert ){

								String	msg = 
									MessageText.getString(
											"crypto.alert.as.warning",
											new String[]{ asn });

								Logger.log( new LogAlert( false, LogAlert.AT_WARNING, msg ));
							}
						}
					}
				}
			}
		}

		// set ui.toolbar.uiswitcher based on instructions from tracker
		// Really shouldn't be in VersionCheck client, but instead have some 
		// listener and have the code elsewhere.  Simply calling 
		//getVersionCheckInfo from "code elsewhere" (to get the cached result) 
		//caused a deadlock at startup.
		Long lEnabledUISwitcher = (Long) reply.get("ui.toolbar.uiswitcher");
		if (lEnabledUISwitcher != null) {
			COConfigurationManager.setBooleanDefault("ui.toolbar.uiswitcher",
					lEnabledUISwitcher.longValue() == 1);
		}
	}

	public InetAddress
	getExternalIpAddressHTTP(
		boolean		v6 )

		throws Exception
	{
		Map reply = executeHTTP( new HashMap(), v6 );

		byte[] address = (byte[])reply.get( "source_ip_address" );

		return( address==null?null:InetAddress.getByName( new String( address )));
	}

	public InetAddress
	getExternalIpAddressTCP(
		InetAddress	 	bind_ip,
		int				bind_port,
		boolean			v6 )

		throws Exception
	{
		Map reply = executeTCP( new HashMap(), bind_ip, bind_port, v6 );

		byte[] address = (byte[])reply.get( "source_ip_address" );

		return( address==null?null:InetAddress.getByName( new String( address )));
	}

	public InetAddress
	getExternalIpAddressUDP(
		InetAddress	 	bind_ip,
		int				bind_port,
		boolean			v6 )

		throws Exception
	{
		Map reply = executeUDP( new HashMap(), bind_ip, bind_port, v6 );

		byte[] address = (byte[])reply.get( "source_ip_address" );

		return( address==null?null:InetAddress.getByName( new String( address )));
	}

	protected String
	getHost(
		boolean		v6,
		String		v6_address,
		String		v4_address )
	{
		if ( v6 ){

			v6_address = getTestAddress( true, v6_address );

			try{
				return( InetAddress.getByName( v6_address ).getHostAddress());

			}catch( UnknownHostException e ){

				DNSUtils.DNSUtilsIntf dns_utils = DNSUtils.getSingleton();
				
				if ( dns_utils != null ){
					
					try{
						return( dns_utils.getIPV6ByName( v6_address ).getHostAddress());
	
					}catch( UnknownHostException f ){	
					}
				}
				
				return( v6_address );
			}
		}else{

			v4_address = getTestAddress( false, v4_address );

			return( v4_address );
		}
	}

	private String
	getTestAddress(
		boolean	v6,
		String	address )
	{
		return( COConfigurationManager.getStringParameter( "versioncheck.test.address." + v6, address ));
	}

	/**
	 * Construct the default version check message.
	 * @return message to send
	 */
	public static Map<String,Object> 
	constructVersionCheckMessage( 
		String reason ) 
	{

		//only send if anonymous-check flag is not set

		boolean send_info = COConfigurationManager.getBooleanParameter( "Send Version Info" );

		Map<String,Object> message = new HashMap<String,Object>();

		//always send
		message.put( "appid",   SystemProperties.getApplicationIdentifier());
		message.put( "appname", SystemProperties.getApplicationName());
		message.put( "version", Constants.AZUREUS_VERSION );
		message.put( "first_version", COConfigurationManager.getStringParameter( "First Recorded Version", "" ));
		
		String	sub_ver = Constants.AZUREUS_SUBVER;

		if ( sub_ver.length() > 0 ){
			message.put( "subver", sub_ver );
		}

		if ( COConfigurationManager.getBooleanParameter( "Beta Programme Enabled" )){

			message.put( "beta_prog", "true" );
		}

		message.put( "ui",      COConfigurationManager.getStringParameter( "ui", "unknown" ) );
		message.put( "os",      Constants.OSName );
		message.put( "os_version", System.getProperty( "os.version" ) );
		message.put( "os_arch", System.getProperty( "os.arch" ) );   //see http://lopica.sourceforge.net/os.html
		message.put( "os_arch_dm", System.getProperty( "sun.arch.data.model" ) );  // might be needed to openjdk on osx

		boolean using_phe = COConfigurationManager.getBooleanParameter( "network.transport.encrypted.require" );
		message.put( "using_phe", using_phe ? new Long(1) : new Long(0) );

		message.put( "imode", COConfigurationManager.getStringParameter( "installer.mode", "" ));
		
		//swt stuff
		try {
			Class c = Class.forName( "org.eclipse.swt.SWT" );

			String swt_platform = (String)c.getMethod( "getPlatform", new Class[]{} ).invoke( null, new Object[]{} );
			message.put( "swt_platform", swt_platform );

			Integer swt_version = (Integer)c.getMethod( "getVersion", new Class[]{} ).invoke( null, new Object[]{} );
			message.put( "swt_version", new Long( swt_version.longValue() ) );
		}
		catch( ClassNotFoundException e ) {  /* ignore */ }
		catch( NoClassDefFoundError er ) {  /* ignore */ }
		catch( InvocationTargetException err ) {  /* ignore */ }
		catch( Throwable t ) {  t.printStackTrace();  }


		int last_send_time = COConfigurationManager.getIntParameter( "Send Version Info Last Time", -1 );
		int current_send_time = (int)(SystemTime.getCurrentTime()/1000);    
		COConfigurationManager.setParameter( "Send Version Info Last Time", current_send_time );


		String id = COConfigurationManager.getStringParameter( "ID", null );

		if( id != null && send_info ) {    	
			message.put( "id", id );    

			try{
				byte[] id2 = CryptoManagerFactory.getSingleton().getSecureID();

				message.put( "id2", id2 );

			}catch( Throwable e ){
			}

			if ( last_send_time != -1 && last_send_time < current_send_time ){    	  
				// time since last    	  
				message.put( "tsl", new Long(current_send_time-last_send_time));
			}

			message.put( "reason", reason );

			String  java_version = Constants.JAVA_VERSION;
			if ( java_version == null ){  java_version = "unknown";  }
			message.put( "java", java_version );


			String  java_vendor = System.getProperty( "java.vm.vendor" );
			if ( java_vendor == null ){   java_vendor = "unknown";  }
			message.put( "javavendor", java_vendor );

			int  api_level = Constants.API_LEVEL;
			if ( api_level > 0 ){
				message.put( "api_level", api_level );
			}

			long  max_mem = Runtime.getRuntime().maxMemory()/(1024*1024);
			message.put( "javamx", new Long( max_mem ) );

			String java_rt_name = System.getProperty("java.runtime.name");
			if (java_rt_name != null) {
				message.put( "java_rt_name", java_rt_name);
			}

			String java_rt_version = System.getProperty("java.runtime.version");
			if (java_rt_version != null) {
				message.put( "java_rt_version", java_rt_version);
			}

			OverallStats	stats = StatsFactory.getStats();

			if ( stats != null ){

				//long total_bytes_downloaded 	= stats.getDownloadedBytes();
				//long total_bytes_uploaded		= stats.getUploadedBytes();
				long total_uptime 			= stats.getTotalUpTime();

				//removed due to complaints about anonymous stats collection
				//message.put( "total_bytes_downloaded", new Long( total_bytes_downloaded ) );
				//message.put( "total_bytes_uploaded", new Long( total_bytes_uploaded ) );
				message.put( "total_uptime", new Long( total_uptime ) );
				//message.put( "dlstats", stats.getDownloadStats());
			}

			try{
				int	port = UDPNetworkManager.getSingleton().getUDPNonDataListeningPortNumber();
				
				message.put( "dht", port );
				
			}catch( Throwable e ){

				Debug.out( e );
			}
			
			try{
				NetworkAdminASN current_asn = NetworkAdmin.getSingleton().getCurrentASN();

				message.put( "ip_as", current_asn.getAS());

				String	asn = current_asn.getASName();

				if ( asn.length() > 64 ){

					asn = asn.substring( 0, 64 );
				}

				message.put( "ip_asn", asn );

			}catch( Throwable e ){

				Debug.out( e );
			}

			// send locale, so we can determine which languages need attention
			message.put("locale", Locale.getDefault().toString());
			String originalLocale = System.getProperty("user.language") + "_"
			+ System.getProperty("user.country");
			String variant = System.getProperty("user.variant");
			if (variant != null && variant.length() > 0) {
				originalLocale += "_" + variant;
			}
			message.put("orig_locale", originalLocale);

			// We may want to reply differently if the user is in Beginner mode vs Advanced
			message.put("user_mode",
					COConfigurationManager.getIntParameter("User Mode", -1));

			Set<String> features = UtilitiesImpl.getFeaturesInstalled();

			if ( features.size() > 0 ){

				String str = "";

				for ( String f: features ){
					str += (str.length()==0?"":",") + f;
				}

				message.put( "vzfeatures", str );
			}

			try{
				if ( AzureusCoreFactory.isCoreAvailable() && 
						AzureusCoreFactory.getSingleton().getPluginManager().isInitialized()){

					//installed plugin IDs
					PluginInterface[] plugins = AzureusCoreFactory.getSingleton().getPluginManager().getPluginInterfaces();

					List pids = new ArrayList();

					List vs_data = new ArrayList();

					for (int i=0;i<plugins.length;i++){

						PluginInterface		plugin = plugins[i];

						String  pid = plugin.getPluginID();

						String	info = plugin.getPluginconfig().getPluginStringParameter( "plugin.info" );

						// filter out built-in and core ones
						if ( 	( info != null && info.length() > 0 ) ||
								(	!pid.startsWith( "<" ) && 
										!pid.startsWith( "azbp" ) &&
										!pid.startsWith( "azupdater" ) &&
										!pid.startsWith( "azplatform" ) &&
										!pids.contains( pid ))){

							if ( info != null && info.length() > 0 ){

								if( info.length() < 256 ){

									pid += ":" + info;

								}else{

									Debug.out( "Plugin '" + pid + "' reported excessive info string '" + info + "'" );
								}
							}

							pids.add( pid );
						}

						Map	data = plugin.getPluginconfig().getPluginMapParameter( "plugin.versionserver.data", null );

						if ( data != null ){

							Map payload = new HashMap();

							byte[]	data_bytes = BEncoder.encode( data );

							if ( data_bytes.length > 16*1024 ){

								Debug.out( "Plugin '" + pid + "' reported excessive version server data (length=" + data_bytes.length + ")" );

								payload.put( "error", "data too long: " + data_bytes.length );

							}else{

								payload.put( "data", data_bytes );
							}

							payload.put( "id", pid);
							payload.put( "version", plugin.getPluginVersion());

							vs_data.add( payload );
						}
					}
					message.put( "plugins", pids );

					if ( vs_data.size() > 0 ){

						message.put( "plugin_data", vs_data );
					}
				}
			}catch( Throwable e ){

				Debug.out( e );
			}
		}


		return message;
	}

	public void
	addVersionCheckClientListener(
			boolean triggerStartListener, 
			VersionCheckClientListener l)
	{
		synchronized (listeners) {
			listeners.add(l);

			if (triggerStartListener && startCheckRan) {
				try {
					l.versionCheckStarted(REASON_UPDATE_CHECK_START);
				} catch (Exception e) {
					Debug.out(e);
				}
			}
		}
	}

	public void
	removeVersionCheckClientListener(
		VersionCheckClientListener l)
	{
		synchronized (listeners) {
			listeners.remove(l);
		}
	}


	public static void
	main(
		String[]	args )
	{
		try{
			COConfigurationManager.initialise();

			COConfigurationManager.setParameter( "IPV6 Enable Support", true );
			
			boolean v6= true;

			// Test connectivity.
			if (true) {
				// System.out.println( "UDP:  " + getSingleton().getExternalIpAddressUDP(null,0,v6));
				// System.out.println( "TCP:  " + getSingleton().getExternalIpAddressTCP(null,0,v6));
				System.out.println( "HTTP: " + getSingleton().getExternalIpAddressHTTP(v6));
			}

			Map data = constructVersionCheckMessage(VersionCheckClient.REASON_UPDATE_CHECK_START);
			System.out.println("Sending (pre-initialisation):");
			printDataMap(data);
			System.out.println("-----------");

			System.out.println("Receiving (pre-initialisation):");
			printDataMap(getSingleton().getVersionCheckInfo(VersionCheckClient.REASON_UPDATE_CHECK_START));
			System.out.println("-----------");

			System.out.println();
			System.out.print("Initialising core... ");

			/**
			 * Suppress all of these errors being displayed in the output.
			 * 
			 * These things should be temporary...
			 */
			AzureusCoreImpl.SUPPRESS_CLASSLOADER_ERRORS = true;
			DownloadManagerStateImpl.SUPPRESS_FIXUP_ERRORS = true;

			AzureusCore core = AzureusCoreFactory.create(); 
			core.start();
			System.out.println("done.");
			System.out.println();
			System.out.println("-----------");

			data = constructVersionCheckMessage(VersionCheckClient.REASON_UPDATE_CHECK_START);
			System.out.println("Sending (post-initialisation):");
			printDataMap(data);
			System.out.println("-----------");

			System.out.println("Receiving (post-initialisation):");
			printDataMap(getSingleton().getVersionCheckInfo(VersionCheckClient.REASON_UPDATE_CHECK_START));
			System.out.println("-----------");
			System.out.println();

			System.out.print("Shutting down core... ");
			core.stop();
			System.out.println("done.");

		}catch( Throwable e){
			e.printStackTrace();
		}
	}

	// Used for debugging in main.
	private static void 
	printDataMap(Map map) 
		throws Exception 
	{
		TreeMap res = new TreeMap(map);
		
		Iterator key_itr = map.keySet().iterator();
		
		while (key_itr.hasNext()) {
			Object key = key_itr.next();
			Object val = map.get(key);
			if (val instanceof byte[]) {
				String as_bytes = ByteFormatter.nicePrint((byte[])val);
				String as_text = new String((byte[])val, Constants.BYTE_ENCODING);
				res.put(key, as_text + " [" + as_bytes + "]");
			}
		}

		Iterator entries = res.entrySet().iterator();
		Map.Entry entry;
		while (entries.hasNext()) {
			entry = (Map.Entry)entries.next();
			System.out.print("  ");
			System.out.print(entry.getKey());
			System.out.print(": ");
			System.out.print(entry.getValue());
			System.out.println();
		}
	}
}
