Share session cookie across subdomains

View: New views
17 Messages — Rating Filter:   Alert me  

Share session cookie across subdomains

by Zach Cox :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

I'm working on a site that has a single web app in Tomcat that handles
lots of subdomains via wildcard DNS:
 - site.com
 - www.site.com
 - sub1.site.com
 - sub2.site.com
 - etc.

Basically, the site content gets customized based on the subdomain in
the request, if any.  However I ran into a problem where Tomcat would
use separate JSESSIONID session cookies on each different domain which
causes problems as the user would have multiple sessions if they
switched subdomains.

I found an examle Valve at http://www.esus.be/blog/?p=3 but it
ultimately did not result in a cross-subdomain session cookie (at
least in the Tomcat 6.0.14 I'm using).  I'm posting the Valve that did
actually seem to solve the problem for me below in hopes that it will
help someone else with the same problem in the future.

Basically you end up with a single session cookie with its domain set
to .site.com so the client will send it back in the request for any
subdomain.

Usage:
 - compile CrossSubdomainSessionValve & put it in a .jar file
 - put that .jar file in $CATALINA_HOME/lib directory
 - include a <Valve
className="org.three3s.valves.CrossSubdomainSessionValve"/> in
$CATALINA_HOME/conf/server.xml

If you see/experience any problems please let me know!

Thanks,
Zach


package org.three3s.valves;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.catalina.*;
import org.apache.catalina.connector.*;
import org.apache.catalina.valves.*;
import org.apache.tomcat.util.buf.*;
import org.apache.tomcat.util.http.*;

/** <p>Replaces the domain of the session cookie generated by Tomcat
with a domain that allows that
 * session cookie to be shared across subdomains.  This valve digs
down into the response headers
 * and replaces the Set-Cookie header for the session cookie, instead
of futilely trying to
 * modify an existing Cookie object like the example at
http://www.esus.be/blog/?p=3.  That
 * approach does not work (at least as of Tomcat 6.0.14) because the
 * <code>org.apache.catalina.connector.Response.addCookieInternal</code>
method renders the
 * cookie into the Set-Cookie response header immediately, making any
subsequent modifying calls
 * on the Cookie object ultimately pointless.</p>
 *
 * <p>This results in a single, cross-subdomain session cookie on the
client that allows the
 * session to be shared across all subdomains.  However, see the
{@link getCookieDomain(Request)}
 * method for limits on the subdomains.</p>
 *
 * <p>Note though, that this approach will fail if the response has
already been committed.  Thus,
 * this valve forces Tomcat to generate the session cookie and then
replaces it before invoking
 * the next valve in the chain.  Hopefully this is early enough in the
valve-processing chain
 * that the response will not have already been committed.  You are
advised to define this
 * valve as early as possible in server.xml to ensure that the
response has not already been
 * committed when this valve is invoked.</p>
 *
 * <p>We recommend that you define this valve in server.xml
immediately after the Catalina Engine
 * as follows:
 * <pre>
 * <Engine name="Catalina"...>
 *     <Valve className="org.three3s.valves.CrossSubdomainSessionValve"/>
 * </pre>
 * </p>
 */
public class CrossSubdomainSessionValve extends ValveBase
{
    public CrossSubdomainSessionValve()
    {
        super();
        info = "org.three3s.valves.CrossSubdomainSessionValve/1.0";
    }

    @Override
    public void invoke(Request request, Response response) throws
IOException, ServletException
    {
        //this will cause Request.doGetSession to create the session
cookie if necessary
        request.getSession(true);

        //replace any Tomcat-generated session cookies with our own
        Cookie[] cookies = response.getCookies();
        if (cookies != null)
        {
            for (int i = 0; i < cookies.length; i++)
            {
                Cookie cookie = cookies[i];
                containerLog.debug("CrossSubdomainSessionValve: Cookie
name is " + cookie.getName());
                if (Globals.SESSION_COOKIE_NAME.equals(cookie.getName()))
                    replaceCookie(request, response, cookie);
            }
        }

        //process the next valve
        getNext().invoke(request, response);
    }

    /** Replaces the value of the response header used to set the
specified cookie to a value
     * with the cookie's domain set to the value returned by
<code>getCookieDomain(request)</code>
     *
     * @param request
     * @param response
     * @param cookie cookie to be replaced.
     */
    @SuppressWarnings("unchecked")
    protected void replaceCookie(Request request, Response response,
Cookie cookie)
    {
        //copy the existing session cookie, but use a different domain
        Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue());
        if (cookie.getPath() != null)
            newCookie.setPath(cookie.getPath());
        newCookie.setDomain(getCookieDomain(request));
        newCookie.setMaxAge(cookie.getMaxAge());
        newCookie.setVersion(cookie.getVersion());
        if (cookie.getComment() != null)
            newCookie.setComment(cookie.getComment());
        newCookie.setSecure(cookie.getSecure());

        //if the response has already been committed, our replacement
strategy will have no effect
        if (response.isCommitted())
            containerLog.error("CrossSubdomainSessionValve: response
was already committed!");

        //find the Set-Cookie header for the existing cookie and
replace its value with new cookie
        MimeHeaders headers = response.getCoyoteResponse().getMimeHeaders();
        for (int i = 0, size = headers.size(); i < size; i++)
        {
            if (headers.getName(i).equals("Set-Cookie"))
            {
                MessageBytes value = headers.getValue(i);
                if (value.indexOf(cookie.getName()) >= 0)
                {
                    StringBuffer buffer = new StringBuffer();
                    ServerCookie.appendCookieValue(buffer,
newCookie.getVersion(), newCookie
                            .getName(), newCookie.getValue(),
newCookie.getPath(), newCookie
                            .getDomain(), newCookie.getComment(),
newCookie.getMaxAge(), newCookie
                            .getSecure());
                    containerLog.debug("CrossSubdomainSessionValve:
old Set-Cookie value: "
                            + value.toString());
                    containerLog.debug("CrossSubdomainSessionValve:
new Set-Cookie value: " + buffer);
                    value.setString(buffer.toString());
                }
            }
        }
    }

    /** Returns the last two parts of the specified request's server
name preceded by a dot.
     * Using this as the session cookie's domain allows the session to
be shared across subdomains.
     * Note that this implies the session can only be used with
domains consisting of two or
     * three parts, according to the domain-matching rules specified
in RFC 2109 and RFC 2965.
     *
     * <p>Examples:</p>
     * <ul>
     * <li>foo.com => .foo.com</li>
     * <li>www.foo.com => .foo.com</li>
     * <li>bar.foo.com => .foo.com</li>
     * <li>abc.bar.foo.com => .foo.com - this means cookie won't work
on abc.bar.foo.com!</li>
     * </ul>
     *
     * @param request provides the server name used to create cookie domain.
     * @return the last two parts of the specified request's server
name preceded by a dot.
     */
    protected String getCookieDomain(Request request)
    {
        String cookieDomain = request.getServerName();
        String[] parts = cookieDomain.split("\\.");
        if (parts.length >= 2)
            cookieDomain = parts[parts.length - 2] + "." +
parts[parts.length - 1];
        return "." + cookieDomain;
    }

    public String toString()
    {
        return ("CrossSubdomainSessionValve[container=" +
container.getName() + ']');
    }
}

---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


Re: Share session cookie across subdomains

by Pid-2 :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

how about adding either, a property that allows you to specify the
number of domain parts (accounting for domain.co.uk for example), or the
actual domain?


p





Zach Cox wrote:

> I'm working on a site that has a single web app in Tomcat that handles
> lots of subdomains via wildcard DNS:
>  - site.com
>  - www.site.com
>  - sub1.site.com
>  - sub2.site.com
>  - etc.
>
> Basically, the site content gets customized based on the subdomain in
> the request, if any.  However I ran into a problem where Tomcat would
> use separate JSESSIONID session cookies on each different domain which
> causes problems as the user would have multiple sessions if they
> switched subdomains.
>
> I found an examle Valve at http://www.esus.be/blog/?p=3 but it
> ultimately did not result in a cross-subdomain session cookie (at
> least in the Tomcat 6.0.14 I'm using).  I'm posting the Valve that did
> actually seem to solve the problem for me below in hopes that it will
> help someone else with the same problem in the future.
>
> Basically you end up with a single session cookie with its domain set
> to .site.com so the client will send it back in the request for any
> subdomain.
>
> Usage:
>  - compile CrossSubdomainSessionValve & put it in a .jar file
>  - put that .jar file in $CATALINA_HOME/lib directory
>  - include a <Valve
> className="org.three3s.valves.CrossSubdomainSessionValve"/> in
> $CATALINA_HOME/conf/server.xml
>
> If you see/experience any problems please let me know!
>
> Thanks,
> Zach
>
>
> package org.three3s.valves;
>
> import java.io.*;
>
> import javax.servlet.*;
> import javax.servlet.http.*;
>
> import org.apache.catalina.*;
> import org.apache.catalina.connector.*;
> import org.apache.catalina.valves.*;
> import org.apache.tomcat.util.buf.*;
> import org.apache.tomcat.util.http.*;
>
> /** <p>Replaces the domain of the session cookie generated by Tomcat
> with a domain that allows that
>  * session cookie to be shared across subdomains.  This valve digs
> down into the response headers
>  * and replaces the Set-Cookie header for the session cookie, instead
> of futilely trying to
>  * modify an existing Cookie object like the example at
> http://www.esus.be/blog/?p=3.  That
>  * approach does not work (at least as of Tomcat 6.0.14) because the
>  * <code>org.apache.catalina.connector.Response.addCookieInternal</code>
> method renders the
>  * cookie into the Set-Cookie response header immediately, making any
> subsequent modifying calls
>  * on the Cookie object ultimately pointless.</p>
>  *
>  * <p>This results in a single, cross-subdomain session cookie on the
> client that allows the
>  * session to be shared across all subdomains.  However, see the
> {@link getCookieDomain(Request)}
>  * method for limits on the subdomains.</p>
>  *
>  * <p>Note though, that this approach will fail if the response has
> already been committed.  Thus,
>  * this valve forces Tomcat to generate the session cookie and then
> replaces it before invoking
>  * the next valve in the chain.  Hopefully this is early enough in the
> valve-processing chain
>  * that the response will not have already been committed.  You are
> advised to define this
>  * valve as early as possible in server.xml to ensure that the
> response has not already been
>  * committed when this valve is invoked.</p>
>  *
>  * <p>We recommend that you define this valve in server.xml
> immediately after the Catalina Engine
>  * as follows:
>  * <pre>
>  * <Engine name="Catalina"...>
>  *     <Valve className="org.three3s.valves.CrossSubdomainSessionValve"/>
>  * </pre>
>  * </p>
>  */
> public class CrossSubdomainSessionValve extends ValveBase
> {
>     public CrossSubdomainSessionValve()
>     {
>         super();
>         info = "org.three3s.valves.CrossSubdomainSessionValve/1.0";
>     }
>
>     @Override
>     public void invoke(Request request, Response response) throws
> IOException, ServletException
>     {
>         //this will cause Request.doGetSession to create the session
> cookie if necessary
>         request.getSession(true);
>
>         //replace any Tomcat-generated session cookies with our own
>         Cookie[] cookies = response.getCookies();
>         if (cookies != null)
>         {
>             for (int i = 0; i < cookies.length; i++)
>             {
>                 Cookie cookie = cookies[i];
>                 containerLog.debug("CrossSubdomainSessionValve: Cookie
> name is " + cookie.getName());
>                 if (Globals.SESSION_COOKIE_NAME.equals(cookie.getName()))
>                     replaceCookie(request, response, cookie);
>             }
>         }
>
>         //process the next valve
>         getNext().invoke(request, response);
>     }
>
>     /** Replaces the value of the response header used to set the
> specified cookie to a value
>      * with the cookie's domain set to the value returned by
> <code>getCookieDomain(request)</code>
>      *
>      * @param request
>      * @param response
>      * @param cookie cookie to be replaced.
>      */
>     @SuppressWarnings("unchecked")
>     protected void replaceCookie(Request request, Response response,
> Cookie cookie)
>     {
>         //copy the existing session cookie, but use a different domain
>         Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue());
>         if (cookie.getPath() != null)
>             newCookie.setPath(cookie.getPath());
>         newCookie.setDomain(getCookieDomain(request));
>         newCookie.setMaxAge(cookie.getMaxAge());
>         newCookie.setVersion(cookie.getVersion());
>         if (cookie.getComment() != null)
>             newCookie.setComment(cookie.getComment());
>         newCookie.setSecure(cookie.getSecure());
>
>         //if the response has already been committed, our replacement
> strategy will have no effect
>         if (response.isCommitted())
>             containerLog.error("CrossSubdomainSessionValve: response
> was already committed!");
>
>         //find the Set-Cookie header for the existing cookie and
> replace its value with new cookie
>         MimeHeaders headers = response.getCoyoteResponse().getMimeHeaders();
>         for (int i = 0, size = headers.size(); i < size; i++)
>         {
>             if (headers.getName(i).equals("Set-Cookie"))
>             {
>                 MessageBytes value = headers.getValue(i);
>                 if (value.indexOf(cookie.getName()) >= 0)
>                 {
>                     StringBuffer buffer = new StringBuffer();
>                     ServerCookie.appendCookieValue(buffer,
> newCookie.getVersion(), newCookie
>                             .getName(), newCookie.getValue(),
> newCookie.getPath(), newCookie
>                             .getDomain(), newCookie.getComment(),
> newCookie.getMaxAge(), newCookie
>                             .getSecure());
>                     containerLog.debug("CrossSubdomainSessionValve:
> old Set-Cookie value: "
>                             + value.toString());
>                     containerLog.debug("CrossSubdomainSessionValve:
> new Set-Cookie value: " + buffer);
>                     value.setString(buffer.toString());
>                 }
>             }
>         }
>     }
>
>     /** Returns the last two parts of the specified request's server
> name preceded by a dot.
>      * Using this as the session cookie's domain allows the session to
> be shared across subdomains.
>      * Note that this implies the session can only be used with
> domains consisting of two or
>      * three parts, according to the domain-matching rules specified
> in RFC 2109 and RFC 2965.
>      *
>      * <p>Examples:</p>
>      * <ul>
>      * <li>foo.com => .foo.com</li>
>      * <li>www.foo.com => .foo.com</li>
>      * <li>bar.foo.com => .foo.com</li>
>      * <li>abc.bar.foo.com => .foo.com - this means cookie won't work
> on abc.bar.foo.com!</li>
>      * </ul>
>      *
>      * @param request provides the server name used to create cookie domain.
>      * @return the last two parts of the specified request's server
> name preceded by a dot.
>      */
>     protected String getCookieDomain(Request request)
>     {
>         String cookieDomain = request.getServerName();
>         String[] parts = cookieDomain.split("\\.");
>         if (parts.length >= 2)
>             cookieDomain = parts[parts.length - 2] + "." +
> parts[parts.length - 1];
>         return "." + cookieDomain;
>     }
>
>     public String toString()
>     {
>         return ("CrossSubdomainSessionValve[container=" +
> container.getName() + ']');
>     }
> }
>
> ---------------------------------------------------------------------
> To start a new topic, e-mail: users@...
> To unsubscribe, e-mail: users-unsubscribe@...
> For additional commands, e-mail: users-help@...
>
>


---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


Re: Share session cookie across subdomains

by Zach Cox :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

That's a really good idea and could definitely be done by modifying the getCookieDomain method.  I whipped that method up pretty quickly for my own purposes - and you're correct, this currently won't work with two-part TLDs like .co.uk.  If I get some spare time (already used 2 days of this project creating the existing valve) I'll add that.  But I think there should be 3 use cases in server.xml:

1) Default - just assume 2 parts in cookie domain to extract from server name, like .domain.com:

<Valve class="className="org.three3s.valves.CrossSubdomainSessionValve" />

2) Specify number of parts in cookie domain to support .domain.co.uk:

<Valve class="className="org.three3s.valves.CrossSubdomainSessionValve" partCount="3" />

#2 could also be used to support multi-level subdomains like a.b.domain.com.  RFCs 2109 & 2965 say that a cookie with domain .domain.com will not be sent in a request to a.b.domain.com; instead it would need to be .b.domain.com.  I haven't tested that so I don't know if that's accurate in practice or not.

You'd need to be careful though if your app supports subdomains with different levels, like a.b.domain.com and c.domain.com etc.  You may end up with session cookies for .b.domain.com and .domain.com and I haven't thought enough about if there would be confusion in a request for c.domain.com or b.comain.com.

3) Specify the actual cookie domain to use:

<Valve class="className="org.three3s.valves.CrossSubdomainSessionValve" cookieDomain=".domain.com" />

So to support #2 and #3 CrossSubdomainSessionValve would need partCount and cookieDomain fields with get/set methods and would then use those, if specified, in the getCookieDomain method.  Until I have time to implement & test I will leave this as an exercise to the reader.  ;)

RE: Share session cookie across subdomains

by Reich, Matthias :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

With your implementation, the valve will have the effect that a session
is created upon first access to a server,
not only when required by the application.
(see also discussion on
http://www.nabble.com/Cookie-less-session-tracking---whats-are-the-downs
ides-tp16738472p16738472.html )

Thus, the valve is only suitable if this behavior is acceptable.
(e.g. if you can be sure that your site is not accessed by search engine
robots, if you have a rather short session timeout and can live with the
overhead of creating many unnecessary session objects, ...)

-- Matthias

-----Original Message-----
From: Zach Cox [mailto:zcox522@...]
Sent: Saturday, April 19, 2008 10:26 PM
To: users@...
Subject: Share session cookie across subdomains

I'm working on a site that has a single web app in Tomcat that handles
lots of subdomains via wildcard DNS:
 - site.com
 - www.site.com
 - sub1.site.com
 - sub2.site.com
 - etc.

Basically, the site content gets customized based on the subdomain in
the request, if any.  However I ran into a problem where Tomcat would
use separate JSESSIONID session cookies on each different domain which
causes problems as the user would have multiple sessions if they
switched subdomains.

I found an examle Valve at http://www.esus.be/blog/?p=3 but it
ultimately did not result in a cross-subdomain session cookie (at
least in the Tomcat 6.0.14 I'm using).  I'm posting the Valve that did
actually seem to solve the problem for me below in hopes that it will
help someone else with the same problem in the future.

Basically you end up with a single session cookie with its domain set
to .site.com so the client will send it back in the request for any
subdomain.

Usage:
 - compile CrossSubdomainSessionValve & put it in a .jar file
 - put that .jar file in $CATALINA_HOME/lib directory
 - include a <Valve
className="org.three3s.valves.CrossSubdomainSessionValve"/> in
$CATALINA_HOME/conf/server.xml

If you see/experience any problems please let me know!

Thanks,
Zach


package org.three3s.valves;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.catalina.*;
import org.apache.catalina.connector.*;
import org.apache.catalina.valves.*;
import org.apache.tomcat.util.buf.*;
import org.apache.tomcat.util.http.*;

/** <p>Replaces the domain of the session cookie generated by Tomcat
with a domain that allows that
 * session cookie to be shared across subdomains.  This valve digs
down into the response headers
 * and replaces the Set-Cookie header for the session cookie, instead
of futilely trying to
 * modify an existing Cookie object like the example at
http://www.esus.be/blog/?p=3.  That
 * approach does not work (at least as of Tomcat 6.0.14) because the
 * <code>org.apache.catalina.connector.Response.addCookieInternal</code>
method renders the
 * cookie into the Set-Cookie response header immediately, making any
subsequent modifying calls
 * on the Cookie object ultimately pointless.</p>
 *
 * <p>This results in a single, cross-subdomain session cookie on the
client that allows the
 * session to be shared across all subdomains.  However, see the
{@link getCookieDomain(Request)}
 * method for limits on the subdomains.</p>
 *
 * <p>Note though, that this approach will fail if the response has
already been committed.  Thus,
 * this valve forces Tomcat to generate the session cookie and then
replaces it before invoking
 * the next valve in the chain.  Hopefully this is early enough in the
valve-processing chain
 * that the response will not have already been committed.  You are
advised to define this
 * valve as early as possible in server.xml to ensure that the
response has not already been
 * committed when this valve is invoked.</p>
 *
 * <p>We recommend that you define this valve in server.xml
immediately after the Catalina Engine
 * as follows:
 * <pre>
 * <Engine name="Catalina"...>
 *     <Valve
className="org.three3s.valves.CrossSubdomainSessionValve"/>
 * </pre>
 * </p>
 */
public class CrossSubdomainSessionValve extends ValveBase
{
    public CrossSubdomainSessionValve()
    {
        super();
        info = "org.three3s.valves.CrossSubdomainSessionValve/1.0";
    }

    @Override
    public void invoke(Request request, Response response) throws
IOException, ServletException
    {
        //this will cause Request.doGetSession to create the session
cookie if necessary
        request.getSession(true);

        //replace any Tomcat-generated session cookies with our own
        Cookie[] cookies = response.getCookies();
        if (cookies != null)
        {
            for (int i = 0; i < cookies.length; i++)
            {
                Cookie cookie = cookies[i];
                containerLog.debug("CrossSubdomainSessionValve: Cookie
name is " + cookie.getName());
                if
(Globals.SESSION_COOKIE_NAME.equals(cookie.getName()))
                    replaceCookie(request, response, cookie);
            }
        }

        //process the next valve
        getNext().invoke(request, response);
    }

    /** Replaces the value of the response header used to set the
specified cookie to a value
     * with the cookie's domain set to the value returned by
<code>getCookieDomain(request)</code>
     *
     * @param request
     * @param response
     * @param cookie cookie to be replaced.
     */
    @SuppressWarnings("unchecked")
    protected void replaceCookie(Request request, Response response,
Cookie cookie)
    {
        //copy the existing session cookie, but use a different domain
        Cookie newCookie = new Cookie(cookie.getName(),
cookie.getValue());
        if (cookie.getPath() != null)
            newCookie.setPath(cookie.getPath());
        newCookie.setDomain(getCookieDomain(request));
        newCookie.setMaxAge(cookie.getMaxAge());
        newCookie.setVersion(cookie.getVersion());
        if (cookie.getComment() != null)
            newCookie.setComment(cookie.getComment());
        newCookie.setSecure(cookie.getSecure());

        //if the response has already been committed, our replacement
strategy will have no effect
        if (response.isCommitted())
            containerLog.error("CrossSubdomainSessionValve: response
was already committed!");

        //find the Set-Cookie header for the existing cookie and
replace its value with new cookie
        MimeHeaders headers =
response.getCoyoteResponse().getMimeHeaders();
        for (int i = 0, size = headers.size(); i < size; i++)
        {
            if (headers.getName(i).equals("Set-Cookie"))
            {
                MessageBytes value = headers.getValue(i);
                if (value.indexOf(cookie.getName()) >= 0)
                {
                    StringBuffer buffer = new StringBuffer();
                    ServerCookie.appendCookieValue(buffer,
newCookie.getVersion(), newCookie
                            .getName(), newCookie.getValue(),
newCookie.getPath(), newCookie
                            .getDomain(), newCookie.getComment(),
newCookie.getMaxAge(), newCookie
                            .getSecure());
                    containerLog.debug("CrossSubdomainSessionValve:
old Set-Cookie value: "
                            + value.toString());
                    containerLog.debug("CrossSubdomainSessionValve:
new Set-Cookie value: " + buffer);
                    value.setString(buffer.toString());
                }
            }
        }
    }

    /** Returns the last two parts of the specified request's server
name preceded by a dot.
     * Using this as the session cookie's domain allows the session to
be shared across subdomains.
     * Note that this implies the session can only be used with
domains consisting of two or
     * three parts, according to the domain-matching rules specified
in RFC 2109 and RFC 2965.
     *
     * <p>Examples:</p>
     * <ul>
     * <li>foo.com => .foo.com</li>
     * <li>www.foo.com => .foo.com</li>
     * <li>bar.foo.com => .foo.com</li>
     * <li>abc.bar.foo.com => .foo.com - this means cookie won't work
on abc.bar.foo.com!</li>
     * </ul>
     *
     * @param request provides the server name used to create cookie
domain.
     * @return the last two parts of the specified request's server
name preceded by a dot.
     */
    protected String getCookieDomain(Request request)
    {
        String cookieDomain = request.getServerName();
        String[] parts = cookieDomain.split("\\.");
        if (parts.length >= 2)
            cookieDomain = parts[parts.length - 2] + "." +
parts[parts.length - 1];
        return "." + cookieDomain;
    }

    public String toString()
    {
        return ("CrossSubdomainSessionValve[container=" +
container.getName() + ']');
    }
}

---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


RE: Share session cookie across subdomains

by Zach Cox :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

That's a good point Matthias - in my case sessions are always created so it's acceptable.  Can you think of a good way to only replace the session cookie when it would be naturally created by the app for those cases where always creating it is not acceptable?

Note that this entire issue would be made much more simple if there was an easy way to override the org.apache.catalina.connector.Request.configureSessionCookie method.  I have not found an easy way to do that.

Thanks,
Zach


Reich, Matthias wrote:
With your implementation, the valve will have the effect that a session
is created upon first access to a server,
not only when required by the application.
(see also discussion on
http://www.nabble.com/Cookie-less-session-tracking---whats-are-the-downs
ides-tp16738472p16738472.html )

Thus, the valve is only suitable if this behavior is acceptable.
(e.g. if you can be sure that your site is not accessed by search engine
robots, if you have a rather short session timeout and can live with the
overhead of creating many unnecessary session objects, ...)

-- Matthias

-----Original Message-----
From: Zach Cox [mailto:zcox522@gmail.com]
Sent: Saturday, April 19, 2008 10:26 PM
To: users@tomcat.apache.org
Subject: Share session cookie across subdomains

I'm working on a site that has a single web app in Tomcat that handles
lots of subdomains via wildcard DNS:
 - site.com
 - www.site.com
 - sub1.site.com
 - sub2.site.com
 - etc.

Basically, the site content gets customized based on the subdomain in
the request, if any.  However I ran into a problem where Tomcat would
use separate JSESSIONID session cookies on each different domain which
causes problems as the user would have multiple sessions if they
switched subdomains.

I found an examle Valve at http://www.esus.be/blog/?p=3 but it
ultimately did not result in a cross-subdomain session cookie (at
least in the Tomcat 6.0.14 I'm using).  I'm posting the Valve that did
actually seem to solve the problem for me below in hopes that it will
help someone else with the same problem in the future.

Basically you end up with a single session cookie with its domain set
to .site.com so the client will send it back in the request for any
subdomain.

Usage:
 - compile CrossSubdomainSessionValve & put it in a .jar file
 - put that .jar file in $CATALINA_HOME/lib directory
 - include a <Valve
className="org.three3s.valves.CrossSubdomainSessionValve"/> in
$CATALINA_HOME/conf/server.xml

If you see/experience any problems please let me know!

Thanks,
Zach


package org.three3s.valves;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.catalina.*;
import org.apache.catalina.connector.*;
import org.apache.catalina.valves.*;
import org.apache.tomcat.util.buf.*;
import org.apache.tomcat.util.http.*;

/** <p>Replaces the domain of the session cookie generated by Tomcat
with a domain that allows that
 * session cookie to be shared across subdomains.  This valve digs
down into the response headers
 * and replaces the Set-Cookie header for the session cookie, instead
of futilely trying to
 * modify an existing Cookie object like the example at
http://www.esus.be/blog/?p=3.  That
 * approach does not work (at least as of Tomcat 6.0.14) because the
 * <code>org.apache.catalina.connector.Response.addCookieInternal</code>
method renders the
 * cookie into the Set-Cookie response header immediately, making any
subsequent modifying calls
 * on the Cookie object ultimately pointless.</p>
 *
 * <p>This results in a single, cross-subdomain session cookie on the
client that allows the
 * session to be shared across all subdomains.  However, see the
{@link getCookieDomain(Request)}
 * method for limits on the subdomains.</p>
 *
 * <p>Note though, that this approach will fail if the response has
already been committed.  Thus,
 * this valve forces Tomcat to generate the session cookie and then
replaces it before invoking
 * the next valve in the chain.  Hopefully this is early enough in the
valve-processing chain
 * that the response will not have already been committed.  You are
advised to define this
 * valve as early as possible in server.xml to ensure that the
response has not already been
 * committed when this valve is invoked.</p>
 *
 * <p>We recommend that you define this valve in server.xml
immediately after the Catalina Engine
 * as follows:
 * <pre>
 * <Engine name="Catalina"...>
 *     <Valve
className="org.three3s.valves.CrossSubdomainSessionValve"/>
 * </pre>
 * </p>
 */
public class CrossSubdomainSessionValve extends ValveBase
{
    public CrossSubdomainSessionValve()
    {
        super();
        info = "org.three3s.valves.CrossSubdomainSessionValve/1.0";
    }

    @Override
    public void invoke(Request request, Response response) throws
IOException, ServletException
    {
        //this will cause Request.doGetSession to create the session
cookie if necessary
        request.getSession(true);

        //replace any Tomcat-generated session cookies with our own
        Cookie[] cookies = response.getCookies();
        if (cookies != null)
        {
            for (int i = 0; i < cookies.length; i++)
            {
                Cookie cookie = cookies[i];
                containerLog.debug("CrossSubdomainSessionValve: Cookie
name is " + cookie.getName());
                if
(Globals.SESSION_COOKIE_NAME.equals(cookie.getName()))
                    replaceCookie(request, response, cookie);
            }
        }

        //process the next valve
        getNext().invoke(request, response);
    }

    /** Replaces the value of the response header used to set the
specified cookie to a value
     * with the cookie's domain set to the value returned by
<code>getCookieDomain(request)</code>
     *
     * @param request
     * @param response
     * @param cookie cookie to be replaced.
     */
    @SuppressWarnings("unchecked")
    protected void replaceCookie(Request request, Response response,
Cookie cookie)
    {
        //copy the existing session cookie, but use a different domain
        Cookie newCookie = new Cookie(cookie.getName(),
cookie.getValue());
        if (cookie.getPath() != null)
            newCookie.setPath(cookie.getPath());
        newCookie.setDomain(getCookieDomain(request));
        newCookie.setMaxAge(cookie.getMaxAge());
        newCookie.setVersion(cookie.getVersion());
        if (cookie.getComment() != null)
            newCookie.setComment(cookie.getComment());
        newCookie.setSecure(cookie.getSecure());

        //if the response has already been committed, our replacement
strategy will have no effect
        if (response.isCommitted())
            containerLog.error("CrossSubdomainSessionValve: response
was already committed!");

        //find the Set-Cookie header for the existing cookie and
replace its value with new cookie
        MimeHeaders headers =
response.getCoyoteResponse().getMimeHeaders();
        for (int i = 0, size = headers.size(); i < size; i++)
        {
            if (headers.getName(i).equals("Set-Cookie"))
            {
                MessageBytes value = headers.getValue(i);
                if (value.indexOf(cookie.getName()) >= 0)
                {
                    StringBuffer buffer = new StringBuffer();
                    ServerCookie.appendCookieValue(buffer,
newCookie.getVersion(), newCookie
                            .getName(), newCookie.getValue(),
newCookie.getPath(), newCookie
                            .getDomain(), newCookie.getComment(),
newCookie.getMaxAge(), newCookie
                            .getSecure());
                    containerLog.debug("CrossSubdomainSessionValve:
old Set-Cookie value: "
                            + value.toString());
                    containerLog.debug("CrossSubdomainSessionValve:
new Set-Cookie value: " + buffer);
                    value.setString(buffer.toString());
                }
            }
        }
    }

    /** Returns the last two parts of the specified request's server
name preceded by a dot.
     * Using this as the session cookie's domain allows the session to
be shared across subdomains.
     * Note that this implies the session can only be used with
domains consisting of two or
     * three parts, according to the domain-matching rules specified
in RFC 2109 and RFC 2965.
     *
     * <p>Examples:</p>
     * <ul>
     * <li>foo.com => .foo.com</li>
     * <li>www.foo.com => .foo.com</li>
     * <li>bar.foo.com => .foo.com</li>
     * <li>abc.bar.foo.com => .foo.com - this means cookie won't work
on abc.bar.foo.com!</li>
     * </ul>
     *
     * @param request provides the server name used to create cookie
domain.
     * @return the last two parts of the specified request's server
name preceded by a dot.
     */
    protected String getCookieDomain(Request request)
    {
        String cookieDomain = request.getServerName();
        String[] parts = cookieDomain.split("\\.");
        if (parts.length >= 2)
            cookieDomain = parts[parts.length - 2] + "." +
parts[parts.length - 1];
        return "." + cookieDomain;
    }

    public String toString()
    {
        return ("CrossSubdomainSessionValve[container=" +
container.getName() + ']');
    }
}

---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org


---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org

Re: Share session cookie across subdomains

by Christopher Schultz-2 :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Zach,

Zach Cox wrote:
| Note that this entire issue would be made much more simple if there was an
| easy way to override the
| org.apache.catalina.connector.Request.configureSessionCookie method.
I have
| not found an easy way to do that.

Have your Valve wrap the Request object with your own class that
overrides that method. Then just move your existing cookie
checking/replacing code into the configureSessionCookie method.

- -chris

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkgMmnsACgkQ9CaO5/Lv0PA/WACeK9Rcw1ltSqoVC/pFXUZFtob3
JYUAnRxnQV+FEtkxqruWMfA0U2p/Qg9O
=ScpV
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


Re: Share session cookie across subdomains

by Zach Cox :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

Hi Chris - I thought about doing that, but that existing Request object could have state that my newly created Request will not have - Request does not have a copy constructor and does not wrap another Request object.  Is it OK to just create a fresh, new, un-initialized Request object & pass it to the next valve in the chain?

Thanks,
Zach


Christopher Schultz-2 wrote:
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Zach,

Zach Cox wrote:
| Note that this entire issue would be made much more simple if there was an
| easy way to override the
| org.apache.catalina.connector.Request.configureSessionCookie method.
I have
| not found an easy way to do that.

Have your Valve wrap the Request object with your own class that
overrides that method. Then just move your existing cookie
checking/replacing code into the configureSessionCookie method.

- -chris

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkgMmnsACgkQ9CaO5/Lv0PA/WACeK9Rcw1ltSqoVC/pFXUZFtob3
JYUAnRxnQV+FEtkxqruWMfA0U2p/Qg9O
=ScpV
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org

Re: Share session cookie across subdomains

by Christopher Schultz-2 :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Zach,

Zach Cox wrote:
| Hi Chris - I thought about doing that, but that existing Request
| object could have state that my newly created Request will not have -
| Request does not have a copy constructor and does not wrap another
| Request object.

That's why you have to write your own Request class that extends the
existing Request class, includes a constructor that takes the "wrapped"
Request, and delegates nearly all operations to this wrapped object.

| Is it OK to just create a fresh, new, un-initialized Request object &
| pass it to the next valve in the chain?

No, you have to do what I have described above.

- -chris

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkgNCmQACgkQ9CaO5/Lv0PARGACgrCxjxUQfVFqxfjilm/wFeQuk
RkIAoJEM7Jp45BpuBTuR1+M/N2x8mHIh
=Dork
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


Re: Share session cookie across subdomains

by Zach Cox :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

That's what I was afraid of - that Request class is huge (2600+ lines) and has lots of fields (50+) some of which are private.  So if I create a Request subclass that accepts another Request in the constructor, there are 2 options:
1) initialize as many of the superclass's fields of the new instance as possible using values from the passed-in Request
2) wrap the passed-in Request & delegate method calls to it (100+ methods)

#1 is very error-prone, as many of the set methods in Request do things other than just setting the instance variable to a new value and in a future release when Request inevitably changes it will be broken.

#2 is also not a good idea: my new Request subclass is not a true wrapper/adapter since I'm overriding one of the methods to get my desired behavior (configureSessionCookie).  That method is not called directly, rather is a protected method called by a public method so I have to pick-and-choose which methods get delegated and which don't, which results in state split across two Request objects which gets very confusing and is also very error-prone.

Overall, I'm going to call the Request API unsuitable for extension and in particular, I'm going to say overriding configureCookieSession in a Request subclass is not a viable solution to the cross-subdomain session cookie problem.  

To make Request suitable for extension I can see 2 possible solutions:
1) Provide a way to specify the fully-qualified name of my Request subclass in server.xml so Tomcat instantiates that instead of org.apache.catalina.connector.Request
2) Provide a copy constructor in org.apache.catalina.connector.Request that initializes its state based on the state of the passed-in Request

If I'm missing something, please correct me.

Thanks,
Zach



Christopher Schultz-2 wrote:
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Zach,

Zach Cox wrote:
| Hi Chris - I thought about doing that, but that existing Request
| object could have state that my newly created Request will not have -
| Request does not have a copy constructor and does not wrap another
| Request object.

That's why you have to write your own Request class that extends the
existing Request class, includes a constructor that takes the "wrapped"
Request, and delegates nearly all operations to this wrapped object.

| Is it OK to just create a fresh, new, un-initialized Request object &
| pass it to the next valve in the chain?

No, you have to do what I have described above.

- -chris

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkgNCmQACgkQ9CaO5/Lv0PARGACgrCxjxUQfVFqxfjilm/wFeQuk
RkIAoJEM7Jp45BpuBTuR1+M/N2x8mHIh
=Dork
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@tomcat.apache.org
To unsubscribe, e-mail: users-unsubscribe@tomcat.apache.org
For additional commands, e-mail: users-help@tomcat.apache.org

Re: Share session cookie across subdomains

by Christopher Schultz-2 :: Rate this Message:

Reply to Author | View Threaded | Show Only this Message

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Zach,

Zach Cox wrote:
| That's what I was afraid of - that Request class is huge (2600+ lines) and
| has lots of fields (50+) some of which are private.  So if I create a
| Request subclass that accepts another Request in the constructor,
there are
| 2 options:
| 1) initialize as many of the superclass's fields of the new instance as
| possible using values from the passed-in Request
| 2) wrap the passed-in Request & delegate method calls to it (100+ methods)

#2 is what you want to do. The constructor should look like this:

public Request(Request wrapped)
{
~    this.wrapped = wrapped;
}

That's it. Every other method should look like this:

public whatever doWhatever(args)
{
~    super.doWhatever(args);
}

| #2 is also not a good idea: my new Request subclass is not a true
| wrapper/adapter since I'm overriding one of the methods to get my desired
| behavior (configureSessionCookie).  That method is not called directly,
| rather is a protected method called by a public method so I have to
| pick-and-choose which methods get delegated and which don't, which results
| in state split across two Request objects which gets very confusing and is
| also very error-prone.

Just override the protected method. Protected methods were made to be
overridden. You are doing the exact same thing that is encouraged by the
Servlet API itself (see javax.servlet.http.HttpServletRequestWrapper).

| Overall, I'm going to call the Request API unsuitable for extension and in
| particular, I'm going to say overriding configureCookieSession in a
Request
| subclass is not a viable solution to the cross-subdomain session cookie
| problem.

Okay, happy coding.

| To make Request suitable for extension I can see 2 possible solutions:
| 1) Provide a way to specify the fully-qualified name of my Request
subclass
| in server.xml so Tomcat instantiates that instead of
| org.apache.catalina.connector.Request

You won't have to do this if you modify Request.java directly, which is
what you'll have to do in order to...

| 2) Provide a copy constructor in org.apache.catalina.connector.Request
that
| initializes its state based on the state of the passed-in Request

I'd be careful with this. You might not be able to get some of the
internal state this way.

If I were you, I'd start by submitting a bugzilla enhancement and patch
to achieve #1 above (a way to specify the class name for the request
class). Right now, each connector has "new Request()" hard-coded into
its constructor. This won't be an easy task, as you will have to either
modify the arguments to the connector's constructor (yuck), add a static
class field to specify the Request class to use (and break convention
within the Tomcat code), and in either case you'll have to modify every
connector implementation (or, at least the ones you are currently using)
/and/ any code that interacts with the connectors (because of the new
constructor args, new static field, whatever).

Trust me. Use a Valve even if you don't want to use a wrapper as I
suggest. Otherwise, you have to modify Tomcat code for each release,
which is horribly unmaintainable.

- -chris

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkgN6rkACgkQ9CaO5/Lv0PCV6wCfV9Yh/wcd9aksuk1KXEUN1G9z
O2AAnj6c8dkK1x/APNSyCrmLYcPoe1Ef
=Px/z
-----END PGP SIGNATURE-----

---------------------------------------------------------------------
To start a new topic, e-mail: users@...
To unsubscribe, e-mail: users-unsubscribe@...
For additional commands, e-mail: users-help@...


Re: Share session cookie across subdomains

by Zach Cox :: Rate this Message: