package com.digitalsanctuary.atg.servlet.pipeline;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.regex.Pattern;

import javax.servlet.ServletException;

import atg.nucleus.ServiceException;
import atg.service.perfmonitor.PerformanceMonitor;
import atg.servlet.DynamoHttpServletRequest;
import atg.servlet.DynamoHttpServletResponse;
import atg.servlet.pipeline.InsertableServletImpl;

/**
 * @author Devon Hillard
 * 
 * Incoming requests which have passed through one or more proxies will have a remoteAddr property of the request set to
 * the IP of the last proxy which the request passed through. Proxies should maintain the original request IP, as well
 * as any previous proxy IP addresses within an extended request header called X-FORWARDED-FOR. This class looks to see
 * if that header exists and is populated, and if so, takes the left-most IP address (which should be the user's source
 * IP address) and places it in the request's remoteAddr property, replacing the proxy IP current in there. We currently
 * don't care about the proxy's IP address, and the address of the user is more useful for auditing, session security,
 * and all other processes currently in place. Since all incoming requests are typically proxied by Akamai servers, we
 * need this functionality in order to use ATG Dynamo's built-in session security mechanism which verifies request IP
 * addresses against the IP address which spawned the session. This pipeline servlet has to go in front of the
 * SessionManager
 * 
 */
public class ProxyIPFixerServlet extends InsertableServletImpl {
    /**
     * The request header name that holds the forwarded chain information.
     */
    private static final String FORWARDED_FOR_HEADER_NAME = "X-FORWARDED-FOR";

    /**
     * The string representing the regular expression to match valid IP addresses. This is set from a properties file.
     */
    private String mIPAddressPatternString;

    /**
     * The actual regular expression compiled Pattern object. This is created at startup, but the doStartService()
     * method.
     */
    private Pattern mIPAddressPattern;

    /**
     * This method handles the component setup.
     * 
     * @see atg.servlet.pipeline.InsertableServletImpl#doStartService()
     * @throws ServiceException
     *                 if do start service fails.
     */
    public void doStartService() throws ServiceException {
	if (isLoggingInfo()) {
	    logInfo("ProxyIPFixerServlet.doStartService: Starting component...");
	}
	super.doStartService();
	this.mIPAddressPattern = Pattern.compile(getIPAddressPatternString());
    }

    /**
     * This method handles the component tear-down.
     * 
     * @see atg.servlet.pipeline.InsertableServletImpl#doStopService()
     * @throws ServiceException
     *                 if do stop service fails.
     */
    public void doStopService() throws ServiceException {
	if (isLoggingInfo()) {
	    logInfo("ProxyIPFixerServlet.doStartService: Stopping component...");
	}
	super.doStopService();
    }

    /**
     * This method takes in the request and response object, as part of the Dynamo servlet pipeline. All processing on
     * those request and response objects takes place here.
     * 
     * @param pRequest
     *                the Dynamo http request object.
     * @param pResponse
     *                the Dynamo http repsonse object.
     * 
     * @see atg.servlet.pipeline.PipelineableServletImpl#service(atg.servlet.DynamoHttpServletRequest,
     *      atg.servlet.DynamoHttpServletResponse)
     * @throws IOException
     *                 on error
     * @throws ServletException
     *                 on error
     */
    public void service(final DynamoHttpServletRequest pRequest, final DynamoHttpServletResponse pResponse)
	    throws IOException, ServletException {
	PerformanceMonitor.startOperation(getAbsoluteName(), "service()");
	// Is this request proxied?
	if (isProxiedRequest(pRequest)) {
	    if (isLoggingDebug()) {
		logDebug("ProxyIPFixerServlet.service: this request appears to have been proxied.");
	    }
	    // Get the original IP address for the request
	    try {
		final String originalRequestIP = getOriginatingIP(pRequest);
		if (isLoggingDebug()) {
		    logDebug("ProxyIPFixerServlet.service: replacing the request's remoteAddr " + "with current value:"
			    + pRequest.getRemoteAddr() + " with the IP from the forwarded for headers with value:"
			    + originalRequestIP + ".");
		}
		// Replace the latest proxy's IP address with the originating IP
		// address for the request in the request's remoteAddr property
		pRequest.setRemoteAddr(originalRequestIP);
	    } catch (final UnknownHostException uhe) {
		if (isLoggingError()) {
		    logError("ProxyIPFixerServlet.service:" + "The value in the forwarded for request header "
			    + "was not parseable into an IP address.", uhe);
		}
	    }
	}
	PerformanceMonitor.endOperation(getAbsoluteName(), "service()");
	// Call super to pass the request on to the next servlet
	super.service(pRequest, pResponse);
    }

    /**
     * This method looks in the passed in request to see if a forwarding header, identified by the static constant
     * FORWARDED_FOR_HEADER_NAME exists, and is populated.
     * 
     * @param pRequest
     *                the http request to examine
     * @return true if the request has been proxied, false, if it has not been (to the best of our ability to determine)
     */
    private boolean isProxiedRequest(final DynamoHttpServletRequest pRequest) {
	// Get the header from the request
	final String forwardedForHeader = pRequest.getHeader(FORWARDED_FOR_HEADER_NAME);
	// Check if we got anything and if we did, check to make sure it isn't zero length
	return (forwardedForHeader != null && forwardedForHeader.length() > 0);
    }

    /**
     * This method pulls the Originating IP out of the header and returns it as a string.
     * 
     * @param pRequest
     *                the http request to examine.
     * @return the originating IP of the request as a String.
     * @throws UnknownHostException
     *                 if the header field cannot be parsed as an IP address.
     */
    private String getOriginatingIP(final DynamoHttpServletRequest pRequest) throws UnknownHostException {
	// Get the header from the request
	final String forwardedForHeader = pRequest.getHeader(FORWARDED_FOR_HEADER_NAME);
	// Check if we got anything
	if (forwardedForHeader != null) {
	    // Get the leftmost address if there are more than one
	    String originatingAddress = null;
	    final int commaIndex = forwardedForHeader.indexOf(',');
	    if (commaIndex > -1) {
		if (isLoggingDebug()) {
		    logDebug("ProxyIPFixerServlet.getOriginatingIP:"
			    + "there are many IPs in the header, getting the first one.");
		}
		originatingAddress = forwardedForHeader.substring(0, commaIndex);
	    } else {
		if (isLoggingDebug()) {
		    logDebug("ProxyIPFixerServlet.getOriginatingIP:" + "there is only one ip in the header, using it.");
		}
		originatingAddress = forwardedForHeader;
	    }
	    // Verify that the value matches what we expect, a quad-IP. This
	    // regex is looking for: 1 to
	    // 3 digits followed by a period, repeating 3 times, and
	    // followed by another set of 1 to 3 digits
	    if (this.mIPAddressPattern.matcher(originatingAddress).matches()) {
		if (isLoggingDebug()) {
		    logDebug("ProxyIPFixerServlet.getOriginatingIP:" + "The string appears to be a valid IP address:"
			    + originatingAddress);
		}
		return originatingAddress;
	    }
	}
	final String msg = "ProxyIPFixerServlet.getOriginatingIP:"
		+ "parsing attempt failed.  Here is what we were working with:" + "  header:" + forwardedForHeader
		+ ".";
	if (isLoggingDebug()) {
	    logDebug(msg);
	}
	// If any of the above checks failed, we were unable to obtain a useable
	// IP address from the request's forwarded ip header list.
	throw new UnknownHostException(msg);
    }

    /**
     * @return the iPAddressPattern
     */
    public String getIPAddressPatternString() {
	return this.mIPAddressPatternString;
    }

    /**
     * @param pAddressPattern
     *                the iPAddressPattern to set
     */
    public void setIPAddressPatternString(final String pAddressPattern) {
	this.mIPAddressPatternString = pAddressPattern;
    }
}
