Teaching Tapestry to use network path references

31/7/2012

2013-03-08, edited to fix:

I finally found the root cause of the problem I was having.

Don't do what I did here. There is no need to use network path references with Tapestry if you configure it correctly. If you use links generated in your application externally, then things are going to break.

What I was really after was a way to run Tapestry on a Tomcat behind an Apache that does the SSL handling. This is easily possible using AJP:

<Connector port="8009" protocol="AJP/1.3" URIEncoding="UTF-8" secure="true" scheme="https" />

The problem is that Tapestry would like you to annotate SSL pages with the @Secure annotation. In production mode, if the annotation is not present on a page, it will generate links like http:my.server:443/mypage. Broken.

The cure is simple: Set the tapestry.secure-enabled configuration symbol to false permanently. This way, Tapestry stops interfering with the URL scheme in any way, which is exactly what we want.


Begin old, outdated post

Every once in a while you stumble over something odd in a technology that you thought you knew well. Yesterday, it was URLs with no protocol/schema: <script src="//api.typeface.com/abcdef" />. Huh?

It turns out that leaving off the protocol is perfectly valid, and the resulting URL is called a network path reference. It makes the URL relative to the protocol used – so if the page was loaded via HTTP, it would load http://api.typeface.com/abcdef, and if it was HTTPS, it would chose https://api.typeface.com/abcdef instead.

This comes in handy when you develop locally using HTTP, but deploy to a machine running HTTPS, and you use extenal scripts such as Typekit's: The relative URL will remove the nasty "insecure content" warnings that modern browsers display when you use content loaded via HTTP from pages loaded via HTTPS.

Looks easy enough, right? For <link>, <script>, <img> tags etc., there in fact is no problem. Also, links just work: <a href="//myapp/mypage">. I was caught by surprise, however, learning that form submits over HTTPS always sent the user to the wrong location after the following redirect, while HTTP worked fine.

I initially thought this was Apache's fault, or maybe mod_proxy_ajp did not understand network path references, but this turned out to be a problem with the Servlet spec. All the way to and including Servlet 3.0, redirects cannot use network path references, so the redirect after a form submission (following the Post-Redirect-Get pattern) will lead your users astray. (This will be fixed in Servlet 3.1.)

The current Jetty version (8.1.5) does not work with network path reference redirects, and neither does Tomcat 6. However, Tomcat 7 supports them without a problem, gently bending the spec in our favor.

The next challenge is making this work with Tapestry. Tapestry uses absolute URLs for form submits and redirects out of the box, meaning that for HTTPS to work, the servlet container must perform the TLS encryption and thus be aware of the HTTPS. If you run a setup with an Apache doing the HTTPS work and accessing a Servlet container via AJP, you're out of luck with Tapestry's defaults.

But you can override the way Tapestry generates URLs with a custom BaseURLSource, making it use network path resources everywhere. This is effectively a copy of the built-in implementation with the URL schema left out:

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.BaseURLSource;
import org.apache.tapestry5.services.Request;

/**
 * BaseURLService that generates network path references, e.g. leaves out
 * http:/https:.
 */
public class NetworkPathReferenceURLSource implements BaseURLSource {
    private final Request request;

    private String hostname;
    private int hostPort;
    private int secureHostPort;

    public NetworkPathReferenceURLSource (Request request,
         @Inject @Symbol(SymbolConstants.HOSTNAME) String hostname,
         @Symbol(SymbolConstants.HOSTPORT) int hostPort,
         @Symbol(SymbolConstants.HOSTPORT_SECURE) int secureHostPort) {
       this.request = request;
       this.hostname = hostname;
       this.hostPort = hostPort;
       this.secureHostPort = secureHostPort;
    }

    public String getBaseURL(boolean secure) {
       int port = secure ? secureHostPort : hostPort;
       String portSuffix = "";

       if (port <= 0) {
         port = request.getServerPort();
         int schemeDefaultPort = request.isSecure() ? 443 : 80;
         portSuffix = port == schemeDefaultPort ? "" : ":" + port;
       } else if (secure && port != 443)
         portSuffix = ":" + port;
       else if (port != 80)
         portSuffix = ":" + port;

       String hostname = "".equals(this.hostname) ? request.getServerName()
          : this.hostname.startsWith("$") ? System.getenv(this.hostname
                 .substring(1)) : this.hostname;

       return String.format("//%s%s", hostname, portSuffix);
    }
}

Now you only have to override the default implementation in your application module (Code in Scala, as our app is in Scala):

def buildNetworkPathReferenceURLSource(@Inject request: Request, 
           @Inject @Symbol(SymbolConstants.HOSTNAME) hostname: String,
           @Symbol(SymbolConstants.HOSTPORT) hostPort: Int, 
           @Symbol(SymbolConstants.HOSTPORT_SECURE) secureHostPort: Int)
           : BaseURLSource = {
       new NetworkPathReferenceURLSource(request, hostname, hostPort, secureHostPort)
    }

def contributeServiceOverride(
        configuration: MappedConfiguration[Class[_], Object], 
        @Local baseUrlSource: BaseURLSource) {
    configuration.add(classOf[BaseURLSource], baseUrlSource)
}

We now have a Tapestry app working nicely on a Tomcat 7 connected to an Apache 2 running only an HTTPS connector, without the application being aware of HTTPS. Also, development using Jetty still works, as network path references work fine over HTTP.

Comments