Absolute URLs in a backend Tomcat server

2012-02-10

Transparently resolving URLs in a proxied Tomcat web app without hard coded base URLs in application code.

While I was integrating Apache Chemistry in a webapp that runs on a backend Tomcat server I stumbled upon the issue that when an application needs to return absolute URLs it should know what the original request looked like.

If your Tomcat server only hides behind 1 front end URL you can use the proxyName and proxyPort attributes of the Connector configuration. Provided that the scheme (http or https) isn't changed.

The setup that was used in this case was a little more complicated.

After submitting a patch to the Apache Chemistry team that used the X-Forwarded-Host and X-Forwarded-Proto to determine the URL from the original request, one team member mentioned the RemoteIpValve. I already looked at that possibility but it didn't solve my problem at first sight, because the main goal of this valve is to determine the client's IP address.

In combination with the correct Apache Server reverse proxy settings it does solve my problem.

Example configuration

The published URLs are https://repo.extranet.frontend.org/cmis and https://repo.frontend.org/cmis, the Apache HTTP Server handles https termination and it's IP address is 192.168.100.254. The backend application URL is http://tomcat.backend.org:8080/cmis

This snippet goes into Apache's httpd.conf:

<Location /cmis>
  RequestHeader set X-Forwarded-Proto "https"
  ProxyPreserveHost
  ProxyPass http://tomcat.backend.org:8080/cmis
  ProxyPassReverse http://tomcat.backend.org:8080/cmis
</Location>

The ProxyPreserveHost directive passes the Original Host HTTP header to the proxied host, for this example repo.extranet.frontend.org or repo.frontend.org, which will be returned by request.getServerName().

The RequestHeader set X-Forwarded-Proto "https" directive explicitly sets the X-Forwarded-Proto request header which will be used by the Tomcat RemoteIpValve.

This snippet goes into Tomcat's server.xml in the Engine section:

<Valve 
   className="org.apache.catalina.valves.RemoteIpValve"
   remoteIpHeader="x-forwarded-for"
   remoteIpProxiesHeader="x-forwarded-by"
   protocolHeader="x-forwarded-proto" />

The valve will calculate the client's IP address, will set request.getScheme() to the value of the X-Forwarded-Proto header (https) and the request.getServerPort() to 443.

Caution: if your reverse proxy has an IP address that doesn't match the networks 10/8, 192.168/16, 169.254/16 or 127/8 you should add an internalProxies attribute to the Valve element. For example:

<Valve 
   className="org.apache.catalina.valves.RemoteIpValve"
   internalProxies="172\.16\.50\.100"
   remoteIpHeader="x-forwarded-for"
   remoteIpProxiesHeader="x-forwarded-by"
   protocolHeader="x-forwarded-proto" />

This should fix it for this environment, I'll be looking into a similar (non application code) solution for Jetty and JBoss.