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; } }