Sunday, October 4, 2009

(Poor man's) Web redirection using a servlet filter

A couple of months ago I bought another domain because we're just in the middle of a re-branding and, obviously, the domain name was one of the first things to choose. We will keep the old domain in order to ease the migration of the systems we're running. Migrating the web was easy but, nowadays, there's some people who's resisting the re-branding process and who's still using our old name and our old domain.

After consulting the company that hosts our old domain, they answered this: 2.5 Eur/Month for web and email redirection. Since we don't need mail redirection, paying a couple of bucks just to send a couple of HTTP headers seemed much to me, that's why I decided to do it myself.

Setting up Sun Java System Application Server on Solaris 10

First of all, I set up a new sparse and shared-IP zone. At the end of the installation I checked the startup script for the Sun Java System Application Server that ships with Solaris 10. If you need a Java application server, you'd better go and install the latest version. As I'm going to run just one servlet filter, using the bundled server seemed like fine to me.

The bundled server is installed in /usr/appserver and isn't either SMF-managed:

$ svcs S84appserv
STATE          STIME    FMRI
legacy_run     Mar_13   lrc:/etc/rc3_d/S84appserv


If you examine the startup script, you'll notice that it looks for domains installed in the /var/appserver/domains directory. Of such domains, this scripts starts those who configured the autostart feature, which is simply an empty file named [domain-dir]/config/autostart.

Creating a domain

To create a domain you need to use the /usr/sbin/asadmin command. In my case, I used the following command:

/usr/sbin/asadmin create-domain --domaindir /var/appserver/domains --adminport 4848 --adminuser admin --instanceport 8080 --savemasterpassword=true my-web

  • --domaindir /var/appserver/domains is necessary to create the domain in the directory searched by the legacy startup script.
  • --adminport and --instanceport are used to configure the ports used respectively by the admin console and the applications you'll deploy in this domain.
  • --savemasterpassword, or an equivalent technique, is necessary because the startup script is non interactive and cannot ask you about the master password.

Once the domain was done, I touched the autostart file to trigger the startup of the domain:

$ touch /var/appserver/domains/my-web/config/autostart

You can now start your domain by using the startup script:

# /etc/rc3.d/S84appserv start

To test your installation you can use your favorite browser to connect to the admin console on the port you specified (in this case, 4848) or to view the welcome file (on port 8080).

Creating a redirecting servlet filter

To redirect clients' HTTP requests to the new domains we just need to send a HTTP 301 status code (moved permanently) and the new location in the Location HTTP header. Omitting the boiler plate code of a standard Java Servlet filter, the code is just a two liners:

response.setHeader("Location", "http://www.newdomain.es/");
response.setStatus(response.SC_MOVED_PERMANENTLY);

Please note that the setHeader and the setStatus method aren't methods of the ServletResponse class: they're included into the HttpServletResponse class.

The last thing to do is mapping the filter in the web module deployment descriptor:

$ cat WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <filter>
        <filter-name>RedirFilter</filter-name>
        <filter-class>es.reacts.filters.RedirFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>RedirFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    </web-app>

The application server bundled with Solaris 10 will complain with an not so intuitive "Unsupported minor.major 49.0 error" if you don't compile your application with a Java 1.4 compiler. Once this was done, you just have to deploy the web module into your server in the root (/) context path.


Setting up the network

The last part of the job was setting up the network. First of all, I removed the DNS A record identifying the www machine (which pointed to our hosting company web server) and substituted with a CNAME pointing to the machine of ours where I deployed the Java web module and then I configured our border router to map port 80 to the port where the Sun Java System Application Server is listening.

Manipulating URIs before redirecting

The filter described so far redirects a request to the root of an application to another URL. The basic problem with this example is that, if a client requests an URI beneath the root of an application, unless such URI is mapped to a valid resource into the application, the application server will return an HTTP 404 status code (not found). The filter semantics is such, indeed: they're executed before the request is handled by its target and, if it does not exist, they're not triggered at all.

To avoid such problems, if you want a catch-all redirection, or if you prefer to process the request before redirecting it, you can deploy a servlet and map it to the URIs you want to catch. And if you want to catch them all, mapping /* is all you need.

The final version of the web.xml of the web module I deployed is the following:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    
    <filter>
        <filter-name>RedirFilter</filter-name>
        <filter-class>es.reacts.filters.RedirFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>RedirFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <servlet>
        <servlet-name>Default</servlet-name>
        <servlet-class>es.reacts.servlets.Default</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Default</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    </web-app>

A Virtual Hosts based solution

As I described in another post, if you're an Apache HTTP Server user, there's an out-of-the-box, more flexible and easier solution: Virtual Hosts.


2 comments:

Geert said...

Using a full blown application server and a custom written servlet looks like overkill to me.
Why not just use the Redirect-directive in Apache httpd?
Light-weight webserver, no Java needed, no compilation...

Grey said...

Hi Geert.

Thanks for your comment. The Apache solution, indeed, is probably simpler to implement and maintain given you've got an Apache running out there.

In this case, I already have a number of Java EE application servers deployed on that particular server while, on the other hand, I have no Apache deployed in the organization. That's why this solution seemed less intrusive to me.

Compilation and deployment aren't a pain, either, because the redir filter is now parametrized.

Bye,
Grey