package httpRecorder.proxy;

/*
 * Disclaimer:
 * The following source code is sample code created by IBM Corporation.  
 * This sample code is not part of any standard IBM product and is provided 
 * to you solely for the purpose of assisting you in the development of your 
 * applications.  The code is provided 'AS IS', without warranty or condition 
 * of any kind.  IBM shall not be liable for any damages arising out of your 
 * use of the sample code, even if IBM has been advised of the possibility of 
 * such damages.
 */

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

/**
 * Simple HTTP proxy that records all the requests and responses between a
 * client - usually a browser - and a server.
 */

public class HttpProxy
implements Runnable
{	
	public static final String CRLF = "\r\n";
	
	private int port;
	
	private boolean mustStop;
	private boolean isAlive;
	
	private boolean multiThreadConnections;
	private boolean storeIpAddresses;
	private int serverSocketTimeout;
	private int clientSocketTimeout;
	
	private IHttpProxyMonitor monitor;
	private int connectionAttemptTimeout;

	private ServerSocket serverSocket;
	private Thread proxyThread;

	public HttpProxy()
	{
		this(0);
	}

	public HttpProxy(int connectionAttemptTimeout)
	{
		this.connectionAttemptTimeout = connectionAttemptTimeout;
		
		port = 80;
		multiThreadConnections = true;
		storeIpAddresses = true;
		mustStop = false;
		isAlive = false;
		clientSocketTimeout = 0;
		serverSocketTimeout = 15000;
		
		monitor = new IHttpProxyMonitor()
		{
			public void started(HttpProxy proxy)
			{
				System.out.println("Proxy started. Listen to port #" + port + ".");
			}

			public void stopped(HttpProxy proxy)
			{
				System.out.println("Proxy stopped.");
			}

			public void handle(Exception e)
			{
				e.printStackTrace();
			}

			public boolean handle(HttpRequest request)
			{
				System.out.println("Request: " + request.getLine());
				return true;
			}

			public boolean handle(HttpResponse response)
			{
				System.out.println("Response: " + response.getLine());
				return true;
			}
		};
	}
	
	public void dispose()
	{
		if(serverSocket != null)
		{
			stopProxy();
			try
			{
				serverSocket.close();
			}
			catch (IOException e)
			{
			}
		}
	}
	
	public int getPort()
	{
		return port;
	}
	
	public void setPort(int port)
	{
		this.port = port;
	}
	
	public IHttpProxyMonitor getMonitor()
	{
		return monitor;
	}

	public void setMonitor(IHttpProxyMonitor monitor)
	{
		if(monitor == null)
			throw new IllegalArgumentException("Monitor cannot be null");
			
		this.monitor = monitor;
	}
	
	public boolean getMultiThreadConnections()
	{
		return multiThreadConnections;
	}

	public void setMultiThreadConnections(boolean multiThreadConnections)
	{
		this.multiThreadConnections = multiThreadConnections;
	}

	public boolean getStoreIpAddresses()
	{
		return storeIpAddresses;
	}

	public void setStoreIpAddresses(boolean storeIpAddresses)
	{
		this.storeIpAddresses = storeIpAddresses;
	}

	public int getConnectionAttemptTimeout()
	{
		return connectionAttemptTimeout;
	}

	public void setClientSocketTimeout(int clientSocketTimeout)
	{
		this.clientSocketTimeout = clientSocketTimeout;
	}

	public int getClientSocketTimeout()
	{
		return clientSocketTimeout;
	}

	public void setServerSocketTimeout(int serverSocketTimeout)
	{
		this.serverSocketTimeout = serverSocketTimeout;
	}

	public int getServerSocketTimeout()
	{
		return serverSocketTimeout;
	}

	public void start()
	{		
		if((proxyThread == null) || (!proxyThread.isAlive()))
		{
			proxyThread = new Thread(this, "HttpProxy");
			proxyThread.setDaemon(true);
		}
			
		proxyThread.start();
	}
	
	public void stopProxy()
	{
		mustStop = true;
	}
	
	public boolean isAlive()
	{
		if(proxyThread == null)
			return isAlive;
			
		return proxyThread.isAlive();
	}
	
	public void run()
	{
		try
		{
			mustStop = false;
			
			try
			{
				ServerSocket auxServerSocket = new ServerSocket(port);
				serverSocket = auxServerSocket;
			}
			catch(RuntimeException rte)
			{
				if(serverSocket == null)
					throw rte;
			}
			serverSocket.setSoTimeout(connectionAttemptTimeout);
			
			isAlive = true;
			try{monitor.started(this);}catch(RuntimeException re){}
				
			while(true)
			{
				if(mustStop)
					break;
					
				try 
				{
					handleConnection(serverSocket.accept());
				}
				catch(InterruptedIOException iioe)
				{
				}
				catch(SocketException se)
				{
					if(!mustStop)
						monitor.handle(se);				
				}
				catch(Exception e)
				{
					monitor.handle(e);
				}
			}
		}
		catch(Exception e)
		{
			monitor.handle(e);
		}
		finally
		{
			isAlive = false;
			try
			{
				if(serverSocket != null)
					serverSocket.close();
			}
			catch(IOException ioe)
			{
			}
			
			try{monitor.stopped(this);}catch(RuntimeException re){}
		}
	}
	
	protected void handleConnection(final Socket client)
	{
		Runnable runnable = new Runnable()
		{
			public void run()
			{
				Socket server = null;
				try
				{
					HttpRequest request = new HttpRequest();
					server = handleRequest(client, request);
					handleResponse(server, client, request);
				}
				catch(InterruptedIOException iioe)
				{
				}
				catch(SocketException se)
				{
					if(!mustStop)
						monitor.handle(se);				
				}
				catch(Exception e)
				{
					monitor.handle(e);
				}
				finally
				{
					try
					{
						if(client != null)
							client.close();
					}
					catch(IOException ioe)
					{
					}
					
					try
					{
						if(server != null)
							server.close();
					}
					catch(IOException ioe)
					{
					}
				}
			}
		};
		
		if(getMultiThreadConnections())
		{
			Thread connectionThread = new Thread(runnable, "HttpProxy_Connection");
			connectionThread.setDaemon(true);
			connectionThread.start();
		}
		else
			runnable.run();
	}
	
	protected Socket handleRequest(Socket client, HttpRequest request)
	throws IOException
	{
		if(client == null)
			return null;
			
		client.setSoTimeout(clientSocketTimeout);
		InputStream inputStream = getInputStream(client);
		request.parse(inputStream);

		if(request.getLine() == null)
			return null;

		InetAddress serverHost = InetAddress.getByName(request.getHost());
		if(getStoreIpAddresses())
		{
			request.setFromIpAddress(client.getInetAddress().getHostAddress());
			request.setToIpAddress(serverHost.getHostAddress());
		}
		
		boolean outputElement = true;
		try{outputElement = monitor.handle(request);}catch(RuntimeException re){}
		if(!outputElement)
			return null;
				
		Socket server = new Socket(serverHost, request.getPort());
		handleElement(request, getOutputStream(server));				
		return server;
	}
	
	protected void handleResponse(Socket server, Socket client, HttpRequest request)
	throws IOException
	{
		if((client == null) || (server == null))
			return;
		
		server.setSoTimeout(serverSocketTimeout);	
		InputStream inputStream = getInputStream(server);
		HttpResponse response = new HttpResponse(request);		
		if(getStoreIpAddresses())
		{
			response.setFromIpAddress(server.getInetAddress().getHostAddress());
			response.setToIpAddress(server.getLocalAddress().getHostAddress());
		}

		response.parse(inputStream);
		
		boolean outputElement = true;
		try{outputElement = monitor.handle(response);}catch(RuntimeException re){}
		if(!outputElement)
			return;

		handleElement(response, getOutputStream(client));
	}
	
	protected void handleElement(HttpElement element, OutputStream outputStream)
	throws IOException
	{
		if(element.getLine() != null)
			outputStream.write(element.getLine().getBytes());
		outputStream.write(CRLF.getBytes());
		
		HttpHeader[] headers = element.getHeaders();
		for (int i = 0; i < headers.length; i++)
		{
			if(!isWritableHeader(headers[i]))
				continue;

			outputStream.write(headers[i].toString().getBytes());
			outputStream.write(CRLF.getBytes());
		}
		outputStream.write(CRLF.getBytes());

		if(element.getBody() != null)
			outputStream.write(element.getBody());
			
		outputStream.flush();
	}
	
	protected boolean isWritableHeader(HttpHeader header)
	{
		return true;
	}
	
	protected InputStream getInputStream(Socket socket)
	throws IOException
	{
		return socket.getInputStream();
	}

	protected OutputStream getOutputStream(Socket socket)
	throws IOException
	{
		return socket.getOutputStream();
	}
}