/*
 * Created on Oct 2, 2008
 * Created by Paul Gardner
 * 
 * Copyright 2008 Vuze, 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; version 2 of the License only.
 * 
 * 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.util.http;

import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import java.io.*;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.gudy.azureus2.core3.security.SESecurityManager;
import org.gudy.azureus2.core3.util.*;

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

public class 
HTTPAuthHelper 
{
	public static final boolean	TRACE	= false;
	
	public static final int MAX_PROCESSORS = 32;
		
	public static final int	CONNECT_TIMEOUT		= 30*1000;
	public static final int READ_TIMEOUT		= 30*1000;

	private HTTPAuthHelper			parent;
	private Map						children	= new HashMap();
	
	private URL						delegate_to;
	private String					delegate_to_host;
	private int						delegate_to_port;
	private boolean					delegate_is_https;
	
	private CopyOnWriteList			listeners = new CopyOnWriteList();
	
	private int		port;
	
	private ServerSocket	server_socket;	
	
	private boolean			http_only_detected;
	
	private Map				cookie_names_set	= new HashMap();
	
	private ThreadPool		thread_pool = new ThreadPool("HTTPSniffer", MAX_PROCESSORS, true );
	
	private List			processors = new ArrayList();
	
	private volatile boolean		destroyed;
		
	public
	HTTPAuthHelper(
		URL			url )
	
		throws Exception
	{
		this( null, url );
	}
	
	protected
	HTTPAuthHelper(
		HTTPAuthHelper			_parent,
		URL						_delegate_to )
	
		throws Exception
	{
		parent				= _parent;
		delegate_to			= _delegate_to;
		
		delegate_to_host	= delegate_to.getHost();
		delegate_is_https	= delegate_to.getProtocol().toLowerCase().equals( "https" );
		delegate_to_port	= delegate_to.getPort()==-1?delegate_to.getDefaultPort():delegate_to.getPort();
					
		server_socket = new ServerSocket();
		
		server_socket.setReuseAddress( true ); 
             
		server_socket.bind( new InetSocketAddress( "127.0.0.1", 0 ));
        
        port = server_socket.getLocalPort();
	}
	
	public void
	start()
	{
        new AEThread2( 
        	"HTTPSniffingProxy: " + delegate_to_host + ":" + delegate_to_port + "/" + delegate_is_https + "/" + port, 
        	true )
        	{
        		public void 
        		run() 
        		{
        			try{
        				while( !destroyed ){
    
        					Socket	socket = server_socket.accept();
        					   
        					socket.setSoTimeout( READ_TIMEOUT );
        					
        					synchronized( HTTPAuthHelper.this ){
        						
        						if ( processors.size() >= MAX_PROCESSORS ){
        							
        							try{
        								Debug.out( "Too many processors" );
        								
        								socket.close();
        								
        							}catch( Throwable e ){
        							}
        						}else{
        							
        							processor proc = new processor( socket );
        							
        							processors.add( proc );
        							
        							proc.start();
        						}
        					}
        				}
        			}catch( Throwable e ){
        				
        				if ( !destroyed ){
        					
        					Debug.printStackTrace( e );
        				}
        			}
        		}
        	}.start();	
	}
	
	public int
	getPort()
	{
		return( port );
	}
	
	public boolean
	wasHTTPOnlyCookieDetected()
	{
		return( http_only_detected );
	}
	
	protected void
	setHTTPOnlyCookieDetected()
	{
		http_only_detected = true;
		
		if ( parent != null ){
			
			parent.setHTTPOnlyCookieDetected();
		}
	}
	
	protected String
	getKey(
		URL		url )
	{
		int child_port = url.getPort()==-1?url.getDefaultPort():url.getPort();
		
		String	key = url.getProtocol() + ":" + url.getHost() + ":" + child_port;

		return( key );
	}
	
	protected HTTPAuthHelper
	getChild(
		String		url_str,
		boolean		optional )
	
		throws Exception
	{
		if ( parent != null ){
	
			return( parent.getChild( url_str,optional ));
		}
	
		String lc_url_str = url_str.toLowerCase();
		
		if ( lc_url_str.startsWith( "http://" ) || lc_url_str.startsWith( "https://")){
			
			URL child_url = new URL( url_str );
			
			String	child_key = getKey( child_url );
			
			if ( child_key.equals( getKey( delegate_to ))){
				
				return( this );
			}
			
			synchronized( this ){
	
				if ( destroyed ){
					
					throw( new Exception( "Destroyed" ));
				}
				
				HTTPAuthHelper child = (HTTPAuthHelper)children.get( child_key );
				
				if ( optional ){
					
						// create children for related domains
					
					String	base_host 	= delegate_to.getHost();
					String	child_host	= child_url.getHost();
					
					int	base_pos = base_host.lastIndexOf( '.' );
					base_pos = base_host.lastIndexOf( '.', base_pos-1 );
					
					int	child_pos = child_host.lastIndexOf( '.' );
					child_pos = child_host.lastIndexOf( '.', child_pos-1 );

					String base_dom 	= base_host.substring( base_pos, base_host.length());
					String child_dom 	= child_host.substring( child_pos, child_host.length());
					
					if ( base_dom.equals( child_dom )){
						
						optional = false;
					}
				}
				
				if ( child == null && !optional ){
				
					child = new HTTPAuthHelper( this, new URL( url_str ));
							
					children.put( child_key, child );
					
					child.start();
				}
				
				return( child );
			}
		}else{
				//relative
			
			return( this );
		}
	}

	protected void
	addSetCookieName(
		String		name,
		String		value )
	{
		if ( parent != null ){
			
			parent.addSetCookieName( name, value );
			
		}else{
			
			boolean	new_entry;
			
			synchronized( cookie_names_set ){
		
				trace( "SetCookieName: " + name );
				
				String old_value = (String)cookie_names_set.put( name, value );
				
				new_entry = old_value==null || !old_value.equals( value );
			}
			
			if ( new_entry ){
				
				Iterator it = listeners.iterator();
				
				while( it.hasNext()){
					
					try{
						((HTTPAuthHelperListener)it.next()).cookieFound( this, name, value );
						
					}catch( Throwable e ){
						
						Debug.printStackTrace(e );
					}
				}
			}
		}
	}
	
	protected boolean
	hasSetCookieName(
		String		name )
	{
		if ( parent != null ){
			
			return( parent.hasSetCookieName( name ));
			
		}else{
		
			synchronized( cookie_names_set ){
				
				trace( "GetCookieName: " + name );
				
				return( cookie_names_set.containsKey( name ));
			}
		}
	}
	
	public void
	addListener(
		HTTPAuthHelperListener		listener )
	{
		listeners.add( listener );
	}
	
	public void
	destroy()
	{
		List	processors_to_destroy;
		List	chidren_to_destroy;
		
		synchronized( this ){
			
			if ( destroyed ){
				
				return;
			}
			
			destroyed = true;
				
			chidren_to_destroy = new ArrayList( children.values());
			
			children.clear();
			
			processors_to_destroy = new ArrayList( processors );
			
			processors.clear();
			
			try{				
				server_socket.close();

			}catch( Throwable e ){	
			}
		}
				
		for (int i=0;i<chidren_to_destroy.size();i++){
			
			try{
				((HTTPAuthHelper)chidren_to_destroy.get(i)).destroy();
				
			}catch( Throwable e ){
			}
		}
		
		for (int i=0;i<processors_to_destroy.size();i++){
			
			try{
				((processor)processors_to_destroy.get(i)).destroy();
			
			}catch( Throwable e ){
			}
		}
	}
	
	protected class
	processor
	{
		private static final String	NL = "\r\n";
		
		private Socket		socket_in;
		private Socket		socket_out;
		
		private transient boolean	destroyed;
		
		protected
		processor(
			Socket		_socket )
		{
			socket_in	= _socket;
		}
		
		protected void
		start()
		{
			thread_pool.run(
				new AERunnable()
				{
					public void
					runSupport()
					{
						try{
							sniff();
							
						}finally{
													
							synchronized( HTTPAuthHelper.this ){

								processors.remove( processor.this );
							}
						}
					}
				});
		}
		
		protected void
		sniff()
		{
			try{
				InputStream is = socket_in.getInputStream();
				
				String request_header = readHeader( is );
								
				connectToDelegate();
				
				process( request_header );
				
			}catch( Throwable e ){
				
				if ( !( e instanceof IOException )){
					
					Debug.out( e );
				}
				
				destroy();
			}
		}
		
		protected void
		connectToDelegate()
		
			throws IOException
		{
			try{
				if ( delegate_is_https ){
					
					TrustManager[] trustAllCerts = new TrustManager[]{
							new X509TrustManager() {
								public java.security.cert.X509Certificate[] getAcceptedIssuers() {
									return null;
								}
								public void checkClientTrusted(
										java.security.cert.X509Certificate[] certs, String authType) {
								}
								public void checkServerTrusted(
										java.security.cert.X509Certificate[] certs, String authType) {
								}
							}
						};
				
					SSLContext sc = SSLContext.getInstance("SSL");
				
					sc.init(null, trustAllCerts, new java.security.SecureRandom());
				
					SSLSocketFactory factory = sc.getSocketFactory();

					try{
						socket_out = factory.createSocket();
						
						socket_out.connect( new InetSocketAddress( delegate_to_host, delegate_to_port ), CONNECT_TIMEOUT );
					
					}catch( SSLException ssl_excep ){
												
						factory = SESecurityManager.installServerCertificates( "AZ-sniffer:" + delegate_to_host + ":" + port, delegate_to_host, delegate_to_port );
						
						socket_out = factory.createSocket();
						
						socket_out.connect( new InetSocketAddress( delegate_to_host, delegate_to_port ), 30*1000 );					
					}
				}else{
					
					socket_out = new Socket();
					
					socket_out.connect( new InetSocketAddress( delegate_to_host, delegate_to_port ), CONNECT_TIMEOUT );
				}
			}catch( Throwable e ){
				
				if ( e instanceof IOException ){
					
					throw((IOException)e );
				}
				
				throw( new IOException( e.toString()));
				
			}finally{
				
				if ( socket_out != null ){
										
					synchronized( this ){
						
						if ( destroyed ){

							try{
								socket_out.close();
								
							}catch( Throwable e ){
								
							}finally{
								
								socket_out = null;
							}
							
							throw( new IOException( "destroyed" ));
						}
					}
				}
			}
		}
		
		protected void
		process(
			String		request_header )
		
			throws Exception
		{
			final OutputStream target_os = socket_out.getOutputStream();
			
			String[]	request_lines = splitHeader( request_header );
			
			String target_url = request_lines[0];
			
			int	space_pos = target_url.indexOf(' ');
			
			target_url = target_url.substring( space_pos ).trim();
			
			space_pos = target_url.indexOf(' ');
			
			target_url = target_url.substring( 0, space_pos ).trim();

			trace( "Page request for " + target_url );
			
			List	cookies_to_remove = new ArrayList();

			for (int i=0;i<request_lines.length;i++){
				
				String	line_out	= request_lines[i];

				String	line_in 	= line_out.trim().toLowerCase();
				
				String[] bits = line_in.split(":");
				
				if ( bits.length >= 2 ){
					
					String	lhs = bits[0].trim();
					
					if ( lhs.equals( "host" )){
						
						String	port_str;
						
						if ( delegate_to_port == 80 || delegate_to_port == 443 ){
						
							port_str = "";
							
						}else{
							
							port_str = ":" + delegate_to_port;
						}
						
						line_out = "Host: " + delegate_to_host + port_str;
						
					}else if ( lhs.equals( "connection" )){
						
						line_out = "Connection: close";
						
					}else if ( lhs.equals( "referer" )){
						
						String page = line_out.substring( line_out.indexOf( ':' )+1).trim();
						
						page = page.substring( page.indexOf( "://") + 3);
						
						int pos = page.indexOf( '/' );
						
						if ( pos >= 0 ){
						
							page = page.substring( pos );
							
						}else{
							
							page = "/";
						}
						
						String	port_str;
						
						if ( delegate_to_port == 80 || delegate_to_port == 443 ){
						
							port_str = "";
							
						}else{
							
							port_str = ":" + delegate_to_port;
						}

						line_out = "Referer: http" + (delegate_is_https?"s":"") + "://" + delegate_to_host + port_str + page;
						
					}else if ( lhs.equals( "cookie" )){

						String cookies_str = line_out.substring( line_out.indexOf( ':' )+1).trim();
						
						String[] cookies = cookies_str.split( ";" );
						
						String	cookies_out = "";
						
						for (int j=0;j<cookies.length;j++){
							
							String	cookie = cookies[j];
							
							String	name = cookie.split( "=" )[0].trim();
							
							if ( hasSetCookieName( name )){
								
								cookies_out += (cookies_out.length()==0?"":"; ") + cookie;
								
							}else{
								
								cookies_to_remove.add( name );
							}
						}
						
						if ( cookies_out.length() > 0 ){
							
							line_out = "Cookie: " + cookies_out;
							
						}else{
							
							line_out = null;
						}
					}
				}
				
				if ( line_out != null ){
					
					trace( "-> " + line_out );
					
					target_os.write((line_out+NL).getBytes());
				}
			}
			
			target_os.write( NL.getBytes());
			
			target_os.flush();
			
			new AEThread2( "HTTPSniffingProxy:proc:2", true )
			{
				public void 
				run() 
				{
					try{
						InputStream	source_is = socket_in.getInputStream();
						
						byte[]	buffer = new byte[32000];
	
						while( !destroyed ){
												
							int	len = source_is.read( buffer );
								
							if ( len <= 0 ){
									
								break;
							}
							
							target_os.write( buffer, 0, len );
							
							trace( "POST:" + new String( buffer, 0, len ));
							
						}
					}catch( Throwable e ){
					}
				}
			}.start();
			
			InputStream	target_is = socket_out.getInputStream();
			
			OutputStream	source_os = socket_in.getOutputStream();
			
			String	reply_header = readHeader( target_is );
			
			String[]	reply_lines = splitHeader( reply_header );
				
			String	content_type	= null;
			String	content_charset	= "ISO-8859-1";
			
			for (int i=0;i<reply_lines.length;i++){
				
				String	line_in 	= reply_lines[i].trim().toLowerCase();
				
				String[] bits = line_in.split(":");
				
				if ( bits.length >= 2 ){

					String	lhs = bits[0].trim();
					
					if ( lhs.equals( "content-type" )){
						
						String rhs = reply_lines[i].substring( line_in.indexOf( ':' ) + 1 ).trim();
								
						String[] x = rhs.split( ";" );
						
						content_type = x[0];
						
						if ( x.length > 1 ){
							
							int	pos = rhs.toLowerCase().indexOf( "charset" );
						
							if ( pos >= 0 ){
							
								String cc = rhs.substring( pos+1 );
							
								pos = cc.indexOf('=');
							
								if ( pos != -1 ){
								
									cc = cc.substring( pos+1 ).trim();
								
									if ( Charset.isSupported( cc )){
																		
										content_charset = cc;
									}
								}
							}
						}
					}
				}
			}
			
			boolean	rewrite 			= false;
			boolean	chunked				= false;
			String	content_encoding	= null;
			
			if ( content_type == null ){
				
				rewrite = true;
				
			}else{
			
				content_type = content_type.toLowerCase();
				
				if ( content_type.indexOf( "text/" ) != -1 ){
					
					rewrite = true;
				}
			}
							
			for (int i=0;i<reply_lines.length;i++){
								
				String	line_out	= reply_lines[i];

				String	line_in 	= line_out.trim().toLowerCase();
				
				String[] bits = line_in.split(":");
				
				if ( bits.length >= 2 ){
					
					String	lhs = bits[0].trim();
					
					if ( lhs.equals( "set-cookie" )){
						
						String	cookies_in = line_out.substring( line_out.indexOf( ':' )+1 );
						
						String[] cookies;
						
						if ( cookies_in.toLowerCase().indexOf( "expires" ) == -1 ){
							
							cookies = cookies_in.split( "," );
							
						}else{
							
							cookies = new String[]{ cookies_in };
						}
						
						String	cookies_out = "";
						
						for (int c=0;c<cookies.length;c++){
							
							String	cookie = cookies[c];
						
							String[]	x = cookie.split( ";" );
							
							String	modified_cookie = "";
							
							for (int j=0;j<x.length;j++){
								
								String entry = x[j].trim();
								
								if ( entry.equalsIgnoreCase( "httponly" )){
									
									setHTTPOnlyCookieDetected();
									
								}else if ( entry.equalsIgnoreCase( "secure" )){
									
								}else if ( entry.toLowerCase().startsWith( "domain" )){
									
										// remove domain restriction so cookie sent to localhost
									
								}else if ( entry.toLowerCase().startsWith( "expires" )){
									
										// force to be session cookie otherwise we'll end up sending
										// cookies from multiple sites to 'localhost'
								}else{
									
									if ( j == 0 ){
										
										int pos = entry.indexOf( '=' );
										
										String name 	= entry.substring( 0, pos ).trim();
										String value 	= entry.substring( pos+1 ).trim();
										
										addSetCookieName( name, value );
									}
									
									modified_cookie += (modified_cookie.length()==0?"":"; ") + entry;
								}
							}
							
							cookies_out += (c==0?"":", " ) + modified_cookie;
						}					
						
						line_out = "Set-Cookie: " + cookies_out;
						
					}else if ( lhs.equals( "set-cookie2" )){
						
							// http://www.ietf.org/rfc/rfc2965.txt
						
							// one or more comma separated
						
						String	cookies_in = line_out.substring( line_out.indexOf( ':' )+1 );
						
						String[] cookies = cookies_in.split( "," );
						
						String	cookies_out = "";
						
						for (int c=0;c<cookies.length;c++){
							
							String	cookie = cookies[c];
							
							String[]	x = cookie.split( ";" );
							
							String	modified_cookie = "";
							
							for (int j=0;j<x.length;j++){
								
								String entry = x[j].trim();
								
								if ( entry.equalsIgnoreCase( "secure" )){
									
								}else if ( entry.equalsIgnoreCase( "discard" )){
	
								}else if ( entry.toLowerCase().startsWith( "domain" )){
																									
								}else if ( entry.toLowerCase().startsWith( "port" )){
																		
								}else{
									
									if ( j == 0 ){
									
										int pos = entry.indexOf( '=' );
										
										String name = entry.substring( 0, pos ).trim();
										
										String value 	= entry.substring( pos+1 ).trim();
										
										addSetCookieName( name, value );
									}
									
									modified_cookie += (modified_cookie.length()==0?"":"; ") + entry;
								}
							}
							
							cookies_out += (c==0?"":", " ) + modified_cookie + "; Discard";
						}
						
						line_out = "Set-Cookie2: " + cookies_out;

					}else if ( lhs.equals( "connection" )){
						
						line_out = "Connection: close";
						
					}else if ( lhs.equals( "location" )){
						
						String page = line_out.substring( line_out.indexOf( ':' )+1).trim();
						
						String child_url = page.trim();
						
						HTTPAuthHelper child = getChild( child_url, false );

						int	pos = page.indexOf( "://" );
						
						if ( pos >= 0 ){
							
								// absolute 
							
							page = page.substring( pos + 3);
						
							pos = page.indexOf( '/' );
						
							if ( pos >= 0 ){
							
								page = page.substring( pos );
							
							}else{
							
								page = "/";
							}
						}else{
							
								// relative. actually illegal as must be absolute
							
							if ( !page.startsWith( "/" )){
								
								String	temp = target_url;
								
								int marker = temp.indexOf( "://" );
								
								if ( marker != -1 ){
									
										// strip out absolute part
									
									temp = temp.substring( marker + 3 );
									
									marker = temp.indexOf( "/" );
									
									if ( marker == -1 ){
										
										temp = "/";
										
									}else{
										
										temp = temp.substring( marker );
									}
								}else{
									
									if ( !temp.startsWith( "/" )){
										
										temp = "/" + temp;
									}
								}
								
								marker = temp.lastIndexOf( "/" );
								
								if ( marker >= 0 ){
									
									temp = temp.substring( 0, marker+1 );
								}
								
								page = temp + page;
							}
						}
						
						line_out = "Location: http://127.0.0.1:" + child.getPort() + page;
						
					}else if ( lhs.equals( "content-encoding" )){
						 
						if ( rewrite ){
							
							String	encoding = bits[1].trim();
								 					
		 					if ( 	encoding.equalsIgnoreCase( "gzip"  ) || 
		 							encoding.equalsIgnoreCase( "deflate" )){
			 									 					
				 				content_encoding = encoding;
			 					
				 				line_out = null;
		 					}
						}
					}else if ( lhs.equals( "content-length" )){

						if ( rewrite ){
							
							line_out = null;
						}
					}else if ( lhs.equals( "transfer-encoding" )){

						if ( bits[1].indexOf( "chunked" ) != -1 ){
							
							chunked = true;
							
							if ( rewrite ){
								
								line_out = null;
							}
						}
	 				}
				}
				
				if ( line_out != null ){
					
					trace( "<- " + line_out );
					
					source_os.write((line_out+NL).getBytes());
				}
			}
			
			for ( int i=0;i<cookies_to_remove.size();i++ ){
				
				String	name = (String)cookies_to_remove.get(i);
				
				if ( !hasSetCookieName( name )){
					
					String	remove_str = "Set-Cookie: " + name + "=X; expires=Sun, 01 Jan 2000 01:00:00 GMT";
					
					trace( "<- (cookie removal) " + remove_str );
					
					source_os.write((remove_str+NL).getBytes());
					
					remove_str = "Set-Cookie2: " + name + "=X; Max-Age=0; Version=1";
					
					trace( "<- (cookie removal) " + remove_str );
					
					source_os.write((remove_str+NL).getBytes());
				}
			}
			
			byte[]	buffer = new byte[32000];
			

			if ( rewrite ){
										
				StringBuffer	sb = new StringBuffer();
					
				if ( chunked ){
					
						// chunking uses ISO-8859-1
					
					while( true ){
						
						int	len = target_is.read( buffer );
						
						if ( len <= 0 ){
							
							break;
						}
						
						sb.append(new String( buffer, 0, len, "ISO-8859-1" ));
					}
					
					StringBuffer	sb_dechunked = new StringBuffer( sb.length());
					
					String chunk = "";
	
					int total_length = 0;
	
					int	sb_pos = 0;
										
					while( sb_pos < sb.length()){
	
						chunk += sb.charAt( sb_pos++ );
	
							// second time around the chunk will be prefixed with NL
							// from end of previous
							// so make sure we ignore this
	
						if ( chunk.endsWith( NL ) && chunk.length() > 2 ){
	
							int semi_pos = chunk.indexOf( ';' );
	
							if ( semi_pos != -1 ){
	
								chunk = chunk.substring( 0, semi_pos );
							}
	
							chunk = chunk.trim();
	
							int chunk_length = Integer.parseInt( chunk, 16 );
	
							if ( chunk_length <= 0 ){
	
								break;
							}
	
							total_length += chunk_length;
	
							if ( total_length > 2*1024*1024 ){
	
								throw (new IOException("Chunk size " + chunk_length
										+ " too large"));
							}
	
							char[] chunk_buffer = new char[chunk_length];
	
							sb.getChars( sb_pos, sb_pos + chunk_length, chunk_buffer, 0 );
	
							sb_dechunked.append( chunk_buffer );
	
							sb_pos += chunk_length;
							
							chunk = "";
						}
					}
					
						// dechunked ISO-8859-1 - unzip if required and then apply correct charset						

					target_is = new ByteArrayInputStream( sb_dechunked.toString().getBytes( "ISO-8859-1" ));
				}
				
				if ( content_encoding != null ){

					if ( content_encoding.equalsIgnoreCase( "gzip"  )){
		 					
						target_is = new GZIPInputStream( target_is );
		 							 				
	 				}else if ( content_encoding.equalsIgnoreCase( "deflate" )){
	 						
	 					target_is = new InflaterInputStream( target_is );
	 				}
	 			}
					
				sb.setLength(0);
					
				while( !destroyed ){
						
					int	len = target_is.read( buffer );
						
					if ( len <= 0 ){
							
						break;
					}
				
					sb.append(new String( buffer, 0, len, content_charset ));
				}
				
				String 	str 	= sb.toString();
				String	lc_str 	= str.toLowerCase();
				
				StringBuffer	result 	= null;
				int				str_pos	= 0;
				
				// FileUtil.writeBytesAsFile( "C:\\temp\\xxx" + new Random().nextInt(100000) + ".txt", str.getBytes());
				
				while( true ){
					
						// http://a.b
					
					int	url_start = str.length() - str_pos >=10?lc_str.indexOf( "http", str_pos ):-1;
					
					if ( url_start == -1 ){
												
						break;
					}
					
					int	match_pos;
					
					if ( lc_str.charAt( url_start + 4 ) == 's' ){
						
						match_pos = url_start + 5;
						
					}else{
						
						match_pos = url_start + 4;
					}
					
					if ( lc_str.substring( match_pos, match_pos+3 ).equals( "://" )){
						
						int	url_end = -1;
						
						for (int i=match_pos+3;;i++){
							
							char c = lc_str.charAt(i);
							
							if ( c == '/' ){
							
								url_end = i+1;
								
								break;
																
							}else if ( c == '.' || c == '-' || c == ':' ){
								
							}else if ( c >= '0' && c <= '9' ){
								
							}else if ( c >= 'a' && c <= 'z' ){
																
							}else{
								
								url_end = i;
								
								break;
							}
							
							if ( i == lc_str.length()-1 ){

								url_end = i;
							}
						}
						
						if ( url_end > url_start ){
							
							String 	url_str = str.substring( url_start, url_end );
							
							boolean	appended = false;
							
							try{	
									// make sure vald URL
								
								URL url = new URL( url_str );
								
								if ( url.getHost().length() > 0 ){
								
									boolean	existing_only = true;
									
										// override if form action or meta
									
									for (int i=url_start-1;i>=0&&url_start-i<512;i--){
										
										if ( lc_str.charAt( i ) == '<' ){
											
											String prefix = lc_str.substring(i, url_start);
											
											if ( prefix.indexOf( "form" ) != -1 ){
												
												existing_only = false;
												
											}else if ( 	prefix.indexOf( "meta" ) != -1 &&
														prefix.indexOf( "http-equiv" ) != -1 ){
												
												existing_only = false;
											}
											
											break;
										}
									}
									
									HTTPAuthHelper child = getChild(  url_str, existing_only );
									
									if ( child != null ){
										
										String replacement = "http://127.0.0.1:" + child.getPort();
										
										if ( url_str.endsWith( "/" )){
											
											replacement += "/";
										}
										
										if ( result == null ){
											
											result = new StringBuffer( str.length());
											
											if ( url_start > 0 ){
												
												result.append( str.subSequence( 0, url_start ));
											}
										}else if ( url_start > str_pos ){
											
											result.append( str.subSequence( str_pos, url_start ));
										}
										
										trace( "Replacing " + url_str + " with " + replacement );
										
										result.append( replacement );
										
										appended = true;
										
									}else{
										
										trace( "    No child for " + url_str );
									}
								}
							}catch( Throwable e ){
								
							}
							
							if ( result != null && !appended ){
								
								result.append( str.subSequence( str_pos, url_end ));
							}
							
							str_pos = url_end;
							
						}else{
							
							break;
						}
					}else{
						
						if ( result != null ){
							
							result.append( str.subSequence( str_pos, match_pos ));
						}
						
						str_pos = match_pos;
					}
				}
				
				if ( result != null ){
							
					if ( str_pos < str.length() ){
						
						result.append( str.subSequence( str_pos, str.length()));
					}

					sb = result;
				}
				
				source_os.write( ( "Content-Length: " + sb.length() + NL ).getBytes());
				
				source_os.write( NL.getBytes());
				
				source_os.write( sb.toString().getBytes( content_charset ));
				
			}else{
				
				source_os.write( NL.getBytes());
							
				while( !destroyed  ){
									
					int	len = target_is.read( buffer );
					
					if ( len <= 0 ){
						
						break;
					}
					
					source_os.write( buffer, 0, len );
				}
			}
		}
		
		protected String
		readHeader(
			InputStream		is )
		
			throws IOException
		{
			String	header = "";
			
			byte[]	buffer = new byte[1];
			
			while( true ){
			
				if ( is.read( buffer ) != 1 ){
					
					break;
				}
				
				header += (char)buffer[0];
				
				if ( header.endsWith( NL + NL )){
					
					break;
				}
			}
			
			return( header );
		}
		
		protected String[]
		splitHeader(
			String		str )
		{
			String[] bits = str.split( NL );
			
			return( bits );
		}
		

		
		protected void
		destroy()
		{
			synchronized( this ){
				
				if ( destroyed ){
					
					return;
				}
				
				destroyed = true;
			}
			
			if ( socket_out != null ){
				
				try{
					socket_out.close();
					
				}catch( Throwable e ){
					
				}
			}
			try{
				socket_in.close();
				
			}catch( Throwable e ){
			}
		}
	}
	
	protected void
	trace(
		String		str )
	{
		if ( TRACE ){
			
			System.out.println( str );
		}
	}
	
	public static void
	main(
		String[]		args )
	{
		try{
			HTTPAuthHelper proxy = new HTTPAuthHelper( new URL( "http://www.sf.net/" ));
			
			proxy.start();
			
			System.out.println( "port=" + proxy.getPort());
			
			while( true ){
				
				Thread.sleep(1000);
			}
		}catch( Throwable e ){
			
			e.printStackTrace();
		}
	}
}
