/*
 *
 * 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.network;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.parosproxy.paros.Constant;


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


    private static ProtocolSocketFactory sslFactory = null;
    private static Protocol protocol = null;

    static {
        
	    try {
	        protocol = Protocol.getProtocol("https");
	        sslFactory = protocol.getSocketFactory();
	    } catch (Exception e) {}
	    // avoid init again if already initialized
	    if (sslFactory == null || !(sslFactory instanceof SSLConnector)) {
	        Protocol.registerProtocol("https", new Protocol("https", (ProtocolSocketFactory) new SSLConnector(), 443));
	    }
    }
    
    private static HttpMethodHelper helper = new HttpMethodHelper();
    private static String userAgent = "";
    
    private HttpClient client = null;
    private HttpClient clientViaProxy = null;
    private ConnectionParam param = null;
    private MultiThreadedHttpConnectionManager httpConnManager = null;
    private MultiThreadedHttpConnectionManager httpConnManagerProxy = null;
    private boolean followRedirect = false;
    private boolean allowState = false;
    
    public HttpSender(ConnectionParam connectionParam, boolean allowState) {
        this.param = connectionParam;
        this.allowState = allowState;

        client = createHttpClient();
        clientViaProxy = createHttpClientViaProxy();
        
        if (this.allowState) {
            checkState();
        }
        addAuth(client);
        addAuth(clientViaProxy);
    }
    
    public static SSLConnector getSSLConnector() {
        return (SSLConnector) protocol.getSocketFactory();
    }
    
    private void checkState() {
        if (param.isHttpStateEnabled()) {            
            client.setState(param.getHttpState());
            clientViaProxy.setState(param.getHttpState());
            client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
            clientViaProxy.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
        } else {
            client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
            clientViaProxy.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
        }
    }
    
    
    private HttpClient createHttpClient() {
        
        httpConnManager = new MultiThreadedHttpConnectionManager(); 
        setCommonManagerParams(httpConnManager);
        return new HttpClient(httpConnManager);   
    }
    
    private HttpClient createHttpClientViaProxy() { 
    
        if (param.getProxyChainName().equals("") ) {
            return createHttpClient();
        }
        
        httpConnManagerProxy = new MultiThreadedHttpConnectionManager();
        setCommonManagerParams(httpConnManagerProxy);
        HttpClient clientProxy = new HttpClient(httpConnManagerProxy);
    	clientProxy.getHostConfiguration().setProxy(param.getProxyChainName(), param.getProxyChainPort());
		
    	if (!param.getProxyChainUserName().equals("")) {
    	    
//    	    NTCredentials credentials = new NTCredentials(
//    	            param.getProxyChainUserName(), param.getProxyChainPassword(), param.getProxyChainName(), param.getProxyChainName());
    	    NTCredentials credentials = new NTCredentials(
    	            param.getProxyChainUserName(), param.getProxyChainPassword(), "",  param.getProxyChainRealm().equals("") ? "" : param.getProxyChainRealm());
//			Below is the original code, but user reported that above code works.
//    	    UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
//    	            param.getProxyChainUserName(), param.getProxyChainPassword());
    	    AuthScope authScope = new AuthScope(param.getProxyChainName(), param.getProxyChainPort(), param.getProxyChainRealm().equals("") ? AuthScope.ANY_REALM : param.getProxyChainRealm());
            
    	    clientProxy.getState().setProxyCredentials(authScope,credentials);
    	}
    	
    	return clientProxy;
    }

    public int executeMethod(HttpMethod method) throws HttpException, IOException {
        String hostName;
        hostName = method.getURI().getHost();
        method.setDoAuthentication(true);

//        if (client.getState() != null) {
//            Cookie[] cookie = client.getState().getCookies();
//            for (int i=0; i<cookie.length;i++) {
//                System.out.println(cookie[i].getName() + ":" + cookie[i].getValue());
//            }
//        }
//
//        if (clientViaProxy.getState() != null) {
//            Cookie[] cookie = clientViaProxy.getState().getCookies();
//            for (int i=0; i<cookie.length;i++) {
//                System.out.println(cookie[i].getName() + ":" + cookie[i].getValue());
//            }
//        }
        
        if (param.isUseProxy(hostName)) {
            return clientViaProxy.executeMethod(method);
        } else {
            return client.executeMethod(method);
        }
    }
    
    public void shutdown() {
        if (httpConnManager != null) {
            httpConnManager.shutdown();
        }
        if (httpConnManagerProxy != null) {
            httpConnManagerProxy.shutdown();
        }
    }
    
    private void addAuth(HttpClient client) {
        List list = param.getListAuth();
        for (int i=0; i<list.size(); i++) {
            HostAuthentication auth = (HostAuthentication) list.get(i);
            AuthScope authScope = null;
            NTCredentials credentials= null;
            try {
                authScope = new AuthScope(auth.getHostName(), auth.getPort(), (auth.getRealm() == null || auth.getRealm().equals("")) ? AuthScope.ANY_REALM : auth.getRealm());
                credentials = new NTCredentials(auth.getUserName(), auth.getPassword(), InetAddress.getLocalHost().getCanonicalHostName(), auth.getHostName());
                client.getState().setCredentials(authScope, credentials);
            } catch (UnknownHostException e1) {
                e1.printStackTrace();
            }
            //client.getState().setCredentials(
            //        new HttpAuthRealm(auth.getHostName(), auth.getPort(), (auth.getRealm().equals("") ? null : auth.getRealm())),
            //        new UsernamePasswordCredentials(auth.getUserName(), auth.getPassword())
            //        );
        }
        
        /*List authPrefs = new Vector(3);
        authPrefs.add(AuthPolicy.NTLM);
        authPrefs.add(AuthPolicy.BASIC);
        authPrefs.add(AuthPolicy.DIGEST);
        client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
        */
    }

    public void sendAndReceive(HttpMessage msg) throws HttpException, IOException {
        sendAndReceive(msg, followRedirect);
        
    }
    
    /**
     * Do not use this unless sure what is doing.  This method works but proxy may skip the pipe without
     * properly handle the filter.
     * 
     * @param msg
     * @param pipe
     * @param buf
     * @throws HttpException
     * @throws IOException
     */
    public void sendAndReceive(HttpMessage msg, HttpOutputStream pipe, byte[] buf) throws HttpException, IOException {
        sendAndReceive(msg, followRedirect, pipe, buf);
        
    }
    
    /**
     * Send and receive a HttpMessage.  
     * @param msg
     * @param isFollowRedirect
     * @throws HttpException
     * @throws IOException
     */
    public void sendAndReceive(HttpMessage msg, boolean isFollowRedirect) throws HttpException, IOException {

        msg.setTimeSentMillis(System.currentTimeMillis());

        try {
            if (!isFollowRedirect || !
                    (msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.POST)
                            || msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.PUT))
            ) {
                send(msg, isFollowRedirect);
                return;
            } else {
                send(msg, false);
            }
            
            HttpMessage temp = msg.cloneAll();
            // POST/PUT method cannot be redirected by library. Need to follow by code
            
            // loop 1 time only because httpclient can handle redirect itself after first GET.
            for (int i=0; i<1
            && (HttpStatusCode.isRedirection(temp.getResponseHeader().getStatusCode())
                    && temp.getResponseHeader().getStatusCode() != HttpStatusCode.NOT_MODIFIED); i++) {
                String location = temp.getResponseHeader().getHeader(HttpHeader.LOCATION);
                URI baseUri = temp.getRequestHeader().getURI();
                URI newLocation = new URI(baseUri, location, false);
                temp.getRequestHeader().setURI(newLocation);
                
                temp.getRequestHeader().setMethod(HttpRequestHeader.GET);
                temp.getRequestHeader().setContentLength(0);
                send(temp, true);
            }
            
            msg.setResponseHeader(temp.getResponseHeader());
            msg.setResponseBody(temp.getResponseBody());

        } finally {
            msg.setTimeElapsedMillis((int) (System.currentTimeMillis()-msg.getTimeSentMillis()));
        }
    }
    
    private void send(HttpMessage msg, boolean isFollowRedirect) throws HttpException, IOException {
        HttpMethod method = null;
        HttpResponseHeader resHeader = null;
        
        try {
            method = runMethod(msg, isFollowRedirect);
	        // successfully executed;
	        resHeader = HttpMethodHelper.getHttpResponseHeader(method);
	        resHeader.setHeader(HttpHeader.TRANSFER_ENCODING, null);	//	replaceAll("Transfer-Encoding: chunked\r\n", "");
	        msg.setResponseHeader(resHeader);
	        msg.getResponseBody().setCharset(resHeader.getCharset());
	        // process response for each listner
	        msg.getResponseBody().setLength(0);
            msg.getResponseBody().append(method.getResponseBody());
        } finally {
	        if (method != null) {
	            method.releaseConnection();
	        }
        }

        
    }
    
	private HttpMethod runMethod(HttpMessage msg, boolean isFollowRedirect) throws HttpException, IOException {
	    int status = -1;
		HttpMethod method = null;
		// no more retry
		modifyUserAgent(msg);
        method = helper.createRequestMethod(msg.getRequestHeader(), msg.getRequestBody());
        method.setFollowRedirects(isFollowRedirect);
        status = this.executeMethod(method);
        if (allowState) {
            if (param.isHttpStateEnabled()) {
                HttpMethodHelper.updateHttpRequestHeaderSent(msg.getRequestHeader(), method);
            }
        }
        return method;
	}
	
	public void setFollowRedirect(boolean followRedirect) {
	    this.followRedirect = followRedirect;
	}
	    
	private void modifyUserAgent(HttpMessage msg) {

	    try {
	        // no modification to user agent if empty
	        if (userAgent.equals("") || msg.getRequestHeader().isEmpty()) {
	            return;
	        }
	        
	        // append new user agent to existing user agent
	        String currentUserAgent = msg.getRequestHeader().getHeader(HttpHeader.USER_AGENT);
	        if (currentUserAgent == null) {
	            currentUserAgent = "";
	        }

	        if (currentUserAgent.indexOf(userAgent) >= 0) {
	            // user agent already in place, exit
	            return;
	        }

	        String delimiter = "";
	        if (!currentUserAgent.equals("") && !currentUserAgent.endsWith(" ")) {
	            delimiter = " ";
	        }
	        
	        currentUserAgent = currentUserAgent + delimiter + userAgent;
	        msg.getRequestHeader().setHeader(HttpHeader.USER_AGENT, currentUserAgent);
	    } catch (Exception e) {}
	}

    /**
     * @return Returns the userAgent.
     */
    public static String getUserAgent() {
        return userAgent;
    }
    
    /**
     * @param userAgent The userAgent to set.
     */
    public static void setUserAgent(String userAgent) {
        HttpSender.userAgent = userAgent;
    }
    
    private void setCommonManagerParams(MultiThreadedHttpConnectionManager mgr) {
        mgr.getParams().setSoTimeout(40000);
        mgr.getParams().setStaleCheckingEnabled(true);
        
        mgr.getParams().setDefaultMaxConnectionsPerHost((Constant.MAX_HOST_CONNECTION > 5) ? 10 : 5*Constant.MAX_HOST_CONNECTION);

        // to use for HttpClient 3.0.1
        //mgr.getParams().setDefaultMaxConnectionsPerHost((Constant.MAX_HOST_CONNECTION > 5) ? 15 : 3*Constant.MAX_HOST_CONNECTION);

        //mgr.getParams().setMaxTotalConnections(mgr.getParams().getDefaultMaxConnectionsPerHost()*10);
        
        //mgr.getParams().setConnectionTimeout(60000);  // use default

    }
    
    /**
     * Send and receive a HttpMessage.  
     * @param msg
     * @param isFollowRedirect
     * @throws HttpException
     * @throws IOException
     */
    public void sendAndReceive(HttpMessage msg, boolean isFollowRedirect, HttpOutputStream pipe, byte[] buf) throws HttpException, IOException {

        msg.setTimeSentMillis(System.currentTimeMillis());

        try {
            if (!isFollowRedirect || !
                    (msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.POST)
                            || msg.getRequestHeader().getMethod().equalsIgnoreCase(HttpRequestHeader.PUT))
            ) {
                send(msg, isFollowRedirect, pipe, buf);
                return;
            } else {
                send(msg, false, pipe, buf);
            }
            
            HttpMessage temp = msg.cloneAll();
            // POST/PUT method cannot be redirected by library. Need to follow by code
            
            // loop 1 time only because httpclient can handle redirect itself after first GET.
            for (int i=0; i<1
            && (HttpStatusCode.isRedirection(temp.getResponseHeader().getStatusCode())
                    && temp.getResponseHeader().getStatusCode() != HttpStatusCode.NOT_MODIFIED); i++) {
                String location = temp.getResponseHeader().getHeader(HttpHeader.LOCATION);
                URI baseUri = temp.getRequestHeader().getURI();
                URI newLocation = new URI(baseUri, location, false);
                temp.getRequestHeader().setURI(newLocation);
                
                temp.getRequestHeader().setMethod(HttpRequestHeader.GET);
                temp.getRequestHeader().setContentLength(0);
                send(temp, true, pipe, buf);
            }
            
            msg.setResponseHeader(temp.getResponseHeader());
            msg.setResponseBody(temp.getResponseBody());

        } finally {
            msg.setTimeElapsedMillis((int) (System.currentTimeMillis()-msg.getTimeSentMillis()));
        }
    }
    
    /**
     * Do not use this unless sure what is doing.  This method works but proxy may skip the pipe without
     * properly handle the filter.
     * 
     * @param msg
     * @param isFollowRedirect
     * @param pipe
     * @param buf
     * @throws HttpException
     * @throws IOException
     */
    private void send(HttpMessage msg, boolean isFollowRedirect, HttpOutputStream pipe, byte[] buf) throws HttpException, IOException {
        HttpMethod method = null;
        HttpResponseHeader resHeader = null;
        
        try {
            method = runMethod(msg, isFollowRedirect);
	        // successfully executed;
	        resHeader = HttpMethodHelper.getHttpResponseHeader(method);
	        resHeader.setHeader(HttpHeader.TRANSFER_ENCODING, null);	//	replaceAll("Transfer-Encoding: chunked\r\n", "");
	        msg.setResponseHeader(resHeader);
	        msg.getResponseBody().setCharset(resHeader.getCharset());
            msg.getResponseBody().setLength(0);

	        // process response for each listner
	        
            pipe.write(msg.getResponseHeader());
            pipe.flush();
            
	        if (msg.getResponseHeader().getContentLength() >= 0 && msg.getResponseHeader().getContentLength() < 20480) {
	            // save time expanding buffer in HttpBody
	            if (msg.getResponseHeader().getContentLength() > 0) {
	                msg.getResponseBody().setBody(method.getResponseBody());
	                pipe.write(msg.getResponseBody());
	                pipe.flush();

	            }
	        } else {
	            //byte[] buf = new byte[4096];
	            InputStream in = method.getResponseBodyAsStream();

	            int len = 0;
	            while (in != null && (len = in.read(buf)) > 0) {
	                pipe.write(buf, 0, len);
	                pipe.flush();

	                msg.getResponseBody().append(buf, len);
	            }
	        }
        } finally {
	        if (method != null) {
	            method.releaseConnection();
	        }
        }

        
    }   
    
}
