Take control of the servlet environment, Part 3

Beware of the cookie monster

In Part 1 of "Take control of the servlet environment," we introduced the Rudimental Servlet Extension Framework (RSEF) and explained how it works. In Part 2, we implemented a concrete example that allowed you to take control of your sessions (away from the servlet engine) and store them into a database. In our concluding article, we'll discuss a nasty little cookie pitfall, how it can bite you, and how you can use RSEF to avoid it. Then, once we have better control over our cookies, we'll use them to implement a new twist on our session solution from last month.

The trouble with cookies

Magical little pieces of data, cookies help a Web server identify and remember a particular Web browser. The first time a browser connects to the server, the server says, "Hi there! Here's a cookie for you, but don't eat it! Show me this cookie each time you come back to visit." Then, during all subsequent browser-to-server requests, the server can identify the visitor.

Behind the scenes, cookies are mapped to domains. Domains are, for the purposes of this discussion, Website addresses. For example, the domain for http://www.yahoo.com/ is www.yahoo.com. Actually, yahoo.com is the domain, and www is a subdomain; herein lies the problem. Cookies are actually mapped to the full path -- domain and subdomain -- and subdomains can go deeper than one layer, for example http://us.f36.mail.yahoo.com/ or http://lw3fd.law3.hotmail.msn.com/.

But why does this present a problem? Suppose you have a visitor surfing your Website at http://www.rudiment.net/. Any cookies that you send to the browser will map to www.rudiment.net. For the purposes of organization, you have segregated a portion of your site at the address http://members.rudiment.net/. As a visitor bounces back and forth between the two addresses, the server cannot share the cookie values. This behavior resembles scope or namespaces in programming. A cookie named "session" might exist in both the www cookie and the members cookie under the rudiment.net domain, with each instance being unique.

But wait, it gets worse. Suppose the user types http://www.members.rudiment.net/ into his or her browser. Assuming that you have this subdomain mapped to http://members.rudiment.net (or your DNS is configured for wildcards) and your Webpages use relative links (both topics beyond the scope of this article), the cookies are now written to the www.members version of the rudiment.net domain instead of the normal www, as the figure below illustrates.

Cookies map to domain and subdomain

Two major problems result:

First, you cannot access any data that you write to the browser at the www.members subdomain if the visitor returns later to the members address. When the browser passes a cookie back to the server, it passes only the values stored in the lowest-level subdomain of the request.

Second, if the user enters the www.members version after having previously visited the members version, and any cookies are still active, your servlet will see two versions of the cookie and will not distinguish between the two. Here's how the "COOKIE" header looks when passed from the browser:

"session=1324;session=1234"

When your servlet looks for the session cookie, it will discover two of them. And if you add or update the session cookie, there's no guarantee that you'll recover the correct value. Why? Because the updated cookie will be written to the www.members version, but when the servlet requests it during the next execution, you might receive the members version first. This is catastrophic! The header value might now look like this:

"session=1324;session=5678"

The Cookie class does contain a getDomain() method, but it doesn't do what you would think. Look at the header, "session=1324;session=5678", above. The browser does not pass in the domain information, so there's no way for the servlet engine to know which subdomain each cookie came from.

RSEF to the rescue

So how do you avoid such a debacle? It's actually quite simple. Behind the scenes, hidden from the servlet, you prepend the cookie names with the current server address before passing them up to the servlet engine. Then, when the servlet requests the cookies, you look them up based on the prepended names. For example, our enigmatic and seemingly redundant session cookies would now be named members.rudiment.net/session and www.members.rudiment.net/session as far as the browser and the server are concerned. But when the servlet asks for the session cookie, it will receive the correct version.

In order to achieve this functionality unobtrusively, you need to implement two more concrete wrappers. You need a ResponseWrapper that will prepend the cookie names, and a RequestWrapper that knows how to retrieve cookies based on the prepended names.

The server address possesses the crucial point of the cookie identification. This address originates from an HTTP header value labeled HOST. The header value is extracted from the request object. However, the ResponseWrapper needs access to the value in order to perform the name-prepending. This is another reason for the complex web of relationships between the wrapper classes (see Part 1).

In order to reduce code duplication, you centralize the functionality for prepending the cookie names in a utility class. The class has one method, getPrefix(), which extracts the HOST value from a request object. It then appends a slash (our arbitrary delimiter) to the address. This ensures that in the case of a null or empty string, you at least have a lone slash to work with:

public class Util
{
    private Util(){}
    private static final String delim = "/";
    protected static String getPrefix( HttpServletRequest request )
    {
        if( null != request )
        {
            String tmp = request.getHeader( "HOST" );
            if( null != tmp )
            {
                return( tmp + delim );
            }
        }
        return( "null" + delim );
    }
}

The next logical piece is the ResponseWrapper. It overrides the addCookie() method to perform the name-prepending before propagating the cookie to the engine:

    public void addCookie( Cookie cookie )
    {
        if( null != _request ) // debugging
        {
            if( null != cookie ) // naughty servlet
            {
                Cookie warped =
                    new Cookie(
                        Util.getPrefix( _request ) + cookie.getName(),
                        cookie.getValue() );
                warped.setMaxAge( cookie.getMaxAge() );
                super.addCookie( warped );
            }
        }
    }

The RequestWrapper strips off the domains at the servlet's request. It also discards any cookies that lack the expected prefix; now, the servlet will not receive two cookies with the same name.

Our version of the RequestWrapper overrides the getCookies() method to iterate through the superclass's set of cookies, stripping off the prefixes and discarding unexpected cookies; it then returns the clean versions:

    public Cookie[] getCookies()
    {
        Vector list = new Vector();
        Cookie[] cookies = super.getCookies();
        if( null != cookies )
        {
            String prefix = Util.getPrefix( super );
            for( int x = 0; x < cookies.length; x++ )
            {
                if( ( null != cookies[x] )
                 && ( cookies[x].getName().startsWith( prefix ) ) )
                {
                    String name = cookies[x].getName();
                    String value = cookies[x].getValue();
                    Cookie warped =
                        new Cookie(
                            name.substring( prefix.length(),
                                            name.length() ),
                            value );
                    warped.setMaxAge( cookies[x].getMaxAge() );
                    list.addElement( warped );
                }
                else
                {
                    // skip it (sub- or super- domain cookie bug)
                }
            }
        }
        Cookie[] gold = new Cookie[ list.size() ];
        list.copyInto( gold );
        return( gold );
    }

That sums up the power and versatility of RSEF. The icing on the cake is that you can seamlessly combine this cookie fix with the database storage solution discussed in Part 2, taking advantage of both. The bootstrap servlet discussed in Part 1 helps you achieve this combination of features.

Back in session

Now that you have a firm grasp on cookies, let's use them to improve the session wrapper discussed in Part 2. If, for some reason, you do not have access to a centralized database for storing your session data, you can store it in the client's cookies. This approach features some advantages; for example, no server-side storage (i.e., database) or server-side garbage collection (expired sessions) are required. However, the session data must transfer from the browser to the server each time a request is issued, and back again when the page is served.

One cookie at a time

The simplest and easiest way to implement the session-stored-in-cookies solution is to use a separate cookie for each name-value pair of session data. In the downloadable source code, available in Resources below, you'll find the class net.rudiment.servlet.session.cookie.SessionWrapper that performs such logic.

In order to differentiate the session-related cookies from other cookies, you will prefix the cookie names with some arbitrary value. The wrapper class contains a simple utility method for this:

    private static final String prefix = "session/";
    private String mangle( String name )
    {
        return( prefix + name );
    }

When placing a piece of data into the session, the wrapper mangles the name and writes it to a cookie:

    public void putValue( String name, Object value )
    {
        String ser = Serialize.objectToString( value );
        Cookie cookie = new Cookie( mangle( name ), ser );
        cookie.setMaxAge( -1 );
        _response.addCookie( cookie );
        // cache it in case it is referenced again
        // during this servlet execution
        super.putValue( name, value );
    }

(Note: The Serialize tool referenced above, and later on, stretches outside the scope of this article. The tool utilizes object I/O streams in conjunction with byte array I/O streams to convert an object into an array of bytes, or vice versa. Then, the byte array is Base64 encoded or decoded and will pass safely between the browser and the server.)

However, requesting a session value makes the process a little tricky. First of all, because it wastes processing time, you don't want to iterate the list of cookies each time in order to locate the requested data. To avoid iteration, you can preload the session data from the cookies when the session object instantiates:

    protected void load()
    {
        Cookie[] cookies = _request.getCookies();
        if( cookies != null )
        {
            for( int x = 0; x < cookies.length; x++ )
            {
                String name = cookies[x].getName();
                Object value = Serialize.objectFromString(
                                    cookies[x].getValue() );
                if( ( null != name ) && ( null != value ) )
                {
                    super.putValue( name, value );
                }
            }
        }
    }

Second, the data might have already been placed into the session earlier in the execution context, in which case you want to return the in-memory version, not the possibly outdated serialized-to-cookie version. Preloading the data prevents that problem as well. Also, note that in addition to writing the data to the cookie, the putValue() method listed above passes the data to the engine. Therefore, the getValue() method simply returns whatever the engine has available.

    public Object getValue( String name )
    {
        return( super.getValue( name ) );
    }

And finally, to remove a session value, or invalidate the entire session, simply expire the appropriate cookies:

    public void removeValue( String name )
    {
        Cookie cookie = new Cookie( name, "" );
        cookie.setMaxAge( 0 );
        _response.addCookie( cookie );
    }
    public void invalidate()
    {
        Cookie[] cookies = _request.getCookies();
        if( cookies != null )
        {
            for( int x = 0; x < cookies.length; x++ )
            {
                removeValue( cookies[x].getName() );
            }
        }
    }

The hidden dilemma

As we just illustrated, storing the session data in the cookies offers a clean and simple solution. However, as we learned the hard way, some browsers strictly limit the number of cookies they will store. Twenty seems to be the lowest common denominator. So, if you plan to store more than 20 data pieces in your sessions, the prior solution won't make the cut.

1 2 Page 1