/*
 * Created on May 25, 2004
 *
 * Paros and its related class files.
 * 
 * Paros is an HTTP/HTTPS proxy for assessing web application security.
 * Copyright (C) 2003-2004 Chinotec Technologies Company
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Clarified Artistic License
 * as published by the Free Software Foundation.
 * 
 * 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
 * Clarified Artistic License for more details.
 * 
 * You should have received a copy of the Clarified Artistic 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 org.parosproxy.paros.core.proxy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.httpclient.URIException;
import org.parosproxy.paros.network.ConnectionParam;
import org.parosproxy.paros.network.HttpBody;
import org.parosproxy.paros.network.HttpHeader;
import org.parosproxy.paros.network.HttpInputStream;
import org.parosproxy.paros.network.HttpMessage;
import org.parosproxy.paros.network.HttpOutputStream;
import org.parosproxy.paros.network.HttpRequestHeader;
import org.parosproxy.paros.network.HttpSender;
import org.parosproxy.paros.network.HttpUtil;



/**
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */

class ProxyThread implements Runnable {

	private static final int		BUFFEREDSTREAM_SIZE = 8192;
	private static final String		CONNECT_HTTP_200 = "HTTP/1.0 200 Connection established\r\nProxy-connection: Keep-alive\r\n\r\n";
	private static ArrayList 		processForwardList = new ArrayList();
	
	// change httpSender to static to be shared among proxies to reuse keep-alive connections

	protected ProxyServer parentServer = null;
	protected ProxyParam proxyParam = null;
	protected ConnectionParam connectionParam = null;
	protected Thread thread = null;
	protected Socket inSocket	= null;
	protected Socket outSocket = null;
	protected HttpInputStream httpIn = null;
	protected HttpOutputStream httpOut = null;
	protected ProxyThread originProcess = this;
	
	private HttpSender 		httpSender = null;
	private BufferedOutputStream forwardOut = null;
	private BufferedInputStream forwardIn = null;
	private boolean disconnect = false;
	private Object semaphore = this;
	private static Object semaphoreSingleton = new Object();

	
	ProxyThread(ProxyServer server, Socket socket) {

		inSocket = socket;
    	try {
			inSocket.setTcpNoDelay(true);
    		inSocket.setSoTimeout(60000);
		} catch (SocketException e) {
		}

		thread = new Thread(this);
		thread.setDaemon(true);
		parentServer = server;
		proxyParam = parentServer.getProxyParam();
		connectionParam = parentServer.getConnectionParam();
		getHttpSender();
	}

	public void start() {
		thread.start();
	}


	
	public void run() {
		boolean isSecure = this instanceof ProxyThreadSSL;
		HttpRequestHeader firstHeader = null;
		
		try {
			httpIn = new HttpInputStream(inSocket);
			httpOut = new HttpOutputStream(inSocket.getOutputStream());
			
			firstHeader = httpIn.readRequestHeader(isSecure);
			if (firstHeader.getMethod().equalsIgnoreCase(HttpRequestHeader.CONNECT)) {
				httpOut.write(CONNECT_HTTP_200);
				processForwardPort();
				
			} else {
				processHttp(firstHeader);
			}
		} catch (IOException e) {
			//e.printStackTrace();

		} finally {
			disconnect();
		}
	}
	
	protected void processHttp(HttpRequestHeader requestHeader) throws IOException {

		HttpBody reqBody = null;
		boolean isFirstRequest = true;
		boolean isSecure = this instanceof ProxyThreadSSL;
		long startTime = System.currentTimeMillis();
		int status = -1;
		HttpMessage msg = null;
		//HttpMethod method = null;
		
		do {


			if (isFirstRequest) {
				isFirstRequest = false;
			} else if (httpIn.available() > 0){
				requestHeader = httpIn.readRequestHeader(isSecure);
			} else {
				// no input available, loop;
				HttpUtil.sleep(30);
				continue;
			}

			msg = new HttpMessage();
			msg.setRequestHeader(requestHeader);
			
			if (msg.getRequestHeader().getContentLength() > 0) {
				reqBody		= httpIn.readBody(requestHeader);
				msg.setRequestBody(reqBody);
				startTime = System.currentTimeMillis();
			}

			modifyHeader(msg);
			
			if (parentServer.isSerialize()) {
			    semaphore = semaphoreSingleton;
			} else {
			    semaphore = this;
			}
			
			synchronized (semaphore) {
			    
			    notifyListenerRequestSend(msg);
			    
			    
			    try {
			        
			        getHttpSender().sendAndReceive(msg);
			        startTime = System.currentTimeMillis();
			        
			        notifyListenerResponseReceive(msg);
			        
			        // write out response header and body
			        
			        httpOut.write(msg.getResponseHeader());

			        startTime = System.currentTimeMillis();
			        
			        if (msg.getResponseBody().length() > 0) {
			            httpOut.write(msg.getResponseBody().getBytes());
			            httpOut.flush();
			            startTime = System.currentTimeMillis();
			        }
			        notifyWrittenToForwardProxy();
			        
			    } catch (IOException e) {
			        //				System.out.println("Secure:" + isSecure + "httpproxy error" + e.getMessage());
			        //				e.printStackTrace();
			        throw e;
			    }
			}	// release semaphore
		} while (!isResponseConnectionClose(msg) && !inSocket.isClosed() && System.currentTimeMillis() - startTime < 3000);
	}
	
	private boolean isResponseConnectionClose(HttpMessage msg) {
		boolean result = true;
		
		if (msg == null || msg.getResponseHeader().isEmpty()) {
		    return result;
		}
		
		if (msg.getResponseHeader().isConnectionClose()) {
		    result = true;
		}
		
		return result;
	}
	
	protected void disconnect() {
		try {
			httpIn.close();
			httpOut.close();
			HttpUtil.closeSocket(inSocket);

		} catch (Exception e) {
			//e.printStackTrace();
		}

		httpSender.shutdown();

	}
	
	private void processForwardPort() {
		
		int len = 0;
		byte[] buf = new byte[BUFFEREDSTREAM_SIZE];
		boolean isError = false;
		long startTime = System.currentTimeMillis();
		
		setDisconnect(false);

		try {

			synchronized (processForwardList) {
				outSocket = new Socket(proxyParam.getProxySSLIp(), proxyParam.getProxySSLPort());
				outSocket.setTcpNoDelay(true);
				processForwardList.add(this);
				forwardOut = new BufferedOutputStream(outSocket.getOutputStream());
				forwardIn = new BufferedInputStream(outSocket.getInputStream());
			}
		} catch (Exception e) {
			//showErrMessage("Error connecting to internal SSL proxy.");
			isError = true;
		}

		try {
			do {
				if (isError) {
					break;
				}

				for (int i=0; i<2 && httpIn.available() > 0; i++) {
					len = httpIn.read(buf);
					if (len > -1) {
						forwardOut.write(buf,0,len);
						forwardOut.flush();
						startTime = System.currentTimeMillis();
					}
				}

				Thread.yield();
				synchronized(this) {
					try {
						if (forwardIn.available() == 0) {
							wait(20);
						}
					} catch (InterruptedException e) {
					}
					
					for (int i=0; i<5 && forwardIn.available() > 0; i++) {
						len = forwardIn.read(buf);
						if (len > -1) {
							httpOut.write(buf,0,len);
							httpOut.flush();
							startTime = System.currentTimeMillis();
						}
					}
					if (forwardIn.available() == 0) {
						setForwardInputBufferEmpty(true);
					}
				}
			} while ((!isDisconnect() || !isForwardInputBufferEmpty()) && System.currentTimeMillis() - startTime < 60000);

		} catch (Exception e) {
			//System.out.println("Error in forward proxy: " + e.getMessage());
		}

		removeFromList();	// end forward port processing
		
		HttpUtil.closeInputStream(forwardIn);
		HttpUtil.closeOutputStream(forwardOut);
		HttpUtil.closeSocket(outSocket);
	}

	protected void removeFromList() {
		synchronized (processForwardList) {
			processForwardList.remove(this);
		}
	}

	protected synchronized boolean isDisconnect() {
		return disconnect;
	}

	protected synchronized void setDisconnect(boolean flag) {
		disconnect = flag;
	}
	
	private boolean isForwardInputBufferEmpty = true;	

	/**
	Set if tunnel buffer empty.
	@param	bufferEmpty true if tunnel buffer is empty
	*/
	protected synchronized void setForwardInputBufferEmpty(boolean bufferEmpty) {
		this.isForwardInputBufferEmpty = bufferEmpty;
	}

	/**
	Check if tunnel buffer empty here
	@return	true = tunnel buffer is empty
	*/
	protected synchronized boolean isForwardInputBufferEmpty() {
		try {
			if (forwardIn.available() > 0) {
				setForwardInputBufferEmpty(false);
			}
		} catch (Exception e) {
		}

		return isForwardInputBufferEmpty;
	}

	protected static ProxyThread getOriginatingProcess (int remotePortUsing) {
		ProxyThread process = null;
		synchronized (processForwardList) {
			for (int i=0;i<processForwardList.size();i++) {
				process = (ProxyThread) processForwardList.get(i);
				if (process.outSocket.getLocalPort() == remotePortUsing) {
					return process;
				}
			}
		}
		return null;
	}
	
	public Thread getThread() {
		return thread;
	}
	
	private void notifyWrittenToForwardProxy() {
		synchronized (originProcess) {
			originProcess.setForwardInputBufferEmpty(false);
			originProcess.notify();
		}
	}

	/**
	 * Go through each observers to process a request in each observers.
	 * The method can be modified in each observers.
	 * @param httpMessage
	 */
	private void notifyListenerRequestSend(HttpMessage httpMessage) {
		ProxyListener listener = null;
		List listenerList = parentServer.getListenerList();
		for (int i=0;i<listenerList.size();i++) {
			listener = (ProxyListener) listenerList.get(i);
			try {
			    listener.onHttpRequestSend(httpMessage);
			} catch (Exception e) {
			}
		}	
	}

	/**
	 * Go thru each observers and process the http message in each observers.
	 * The msg can be changed by each observers.
	 * @param msg
	 */
	private void notifyListenerResponseReceive(HttpMessage httpMessage) {
		ProxyListener listener = null;
		List listenerList = parentServer.getListenerList();
		for (int i=0;i<listenerList.size();i++) {
			listener = (ProxyListener) listenerList.get(i);
			try {
			    listener.onHttpResponseReceive(httpMessage);
			} catch (Exception e) {
			}
		}
	}
	
	private boolean isRecursive(HttpRequestHeader header) {
        boolean isRecursive = false;
        try {
            if (header.getURI().getHost().equals(proxyParam.getProxyIp())) {
                if (header.getURI().getPort() == proxyParam.getProxyPort()) {
                    isRecursive = true;
                }
            }
        } catch (URIException e) {
        }
        return true;
    }
	
	private void modifyHeader(HttpMessage msg) {
	    // avoid returning gzip encoding
		msg.getRequestHeader().setHeader(HttpHeader.ACCEPT_ENCODING,null);

	}
	
	protected HttpSender getHttpSender() {

	    if (httpSender == null) {
		    httpSender = new HttpSender(connectionParam);
		}

	    return httpSender;
	}
}
