You are at: http://i.am/rwald / java / netdynamics / storing the netdynamics session id within a client-side "cookie"

Storing the NetDynamics Session Id Within a Client-Side "Cookie"

Introduction

Within NetDynamics, persistent data about an end-user ("user session objects") are stored on the server-side within the Persistence Engine. NetDynamics uses a "session identifier" to match an incoming HTTP request to the state information stored within the PE. This session identifier is contained in an instance of the spider.session.CSpSessionId class. Along with page session objects, the client's CSpSessionId is one of the values that make up SPIDERSESSION value that is stored within the HTML forms or hrefs generated by NetDynamics. It is important for this SPIDERSESSION value to be passed between hits to the application server, since without it NetDynamics will be unable to track a user through the application.

Yet there are times when it isn't practical to expect the SPIDERSESSION value to be submitted, as when moving back and forth between NetDynamics and non-NetDynamics pages. In this case it would be convenient to store the user's session id within a client-side cookie, so that we can re-enter the user's session in the absence of the SPIDERSESSION value. This document will show you how.

First, a brief note on cookies

Cookies were first introduced by Netscape. Their proposal, which describes the way in which cookies are implemented on most browsers today, can be found at http://home.netscape.com/newsref/std/cookie_spec.html. Briefly, cookies are written from the server to the client using the Set-Cookie HTTP header, which looks like this:

Set-Cookie: name=value [;expires=EEE, dd-MMM-yyyy HH:mm:ss GMT] [;domain=domain] [;path=path] [;secure]

(where brackets denote optional values).

When a browser wants to return a cookie to the server, it uses the Cookie HTTP header, which looks like this:

Cookie: name=value [;name=value]*

Cookies are only returned to URIs within the specified path in the specified domain, and only if they have not yet expired. The absence of an expiration date means the cookie will expire when the browser is closed. Note that all cookies being passed to the server appear within a single Cookie header line. We'll need to parse the individual cookies from that string.

The cookie specification is refined by RFC 2109 (see http://sunsite.auc.dk/RFC/rfc/rfc2109.html, among many others). Using the RFC 2109 specification, the Set-Cookie HTTP header looks like:

Set-Cookie: name=value [;comment=comment] [;max-age=deltaseconds] [;domain=domain] [;path=path] [;secure] [version=vernum]

(where brackets denote optional values.)

When an RFC 2109 compliant browser passes a cookie to the server, the Cookie HTTP header looks like:

Cookie: name=value;$version=vernum;$domain=domain;$path=path

Note that as of this writing, most browsers do not support the RFC 2109 cookie specification. For this reason, one should be sure to include both and expires and a max-age attribute, to ensure broader compatibility. Regardless, due to the naming conventions defined in RFC 2109, cookie names should not begin with a dollar sign ($).

Writing the CSpSessionId to a cookie:

First, we'll have to get the session id and serialize it so we can read it later. We do this as follows:

String getSsidAsCookie() {
  // create a byte array output stream to hold the serialized data
  ByteArrayOutputStream bout = new ByteArrayOutputStream();

  // wrap the byte array output stream in a data output stream,
  // so that we can serialize the ssid
  DataOutputStream dout = new DataOutputStream(bout);

  // get the current session id
  CSpSessionId ssid = CSpider.getUserSessionId();

  // serialize it
  try {
    ssid.serialize(dout);
  } catch (IOException e) {
    CSpLog.send(this,CSpLog.ERROR,"getSsid(): Unable to serialize the ssid ("
                +ssid+") due to "+e+". Returning \"\".");
    return "";
  }
  
  // encode the (binary) serialized data in the printable ASCII subset of Unicode
  String encssid = CSpHttp.escape(bout.toString());
  
  // close the streams, since we're done with them
  try {
    dout.close();
  } catch (IOException e2) {
    CSpLog.send(this,CSpLog.WARNING,"getSsid(): Exception while closing output stream ("
                +e+". Exception will be ignored.");
  }
  
  // create set-cookie header, together with some cache-control headers,
  // since cookies (and pages that write them) should not be cached
  StringBuffer buf = new StringBuffer();

  buf.append("Set-Cookie: ssid=").append(encssid).append(";path=/;domain=.foo.com\r\n");
  // note that to avoid "hard-coding" the domain, you could use 
  // the value returned by
  // CSpider.getWebEnvVar(CSpVar.SERVER_HOST);

  buf.append("Pragma: no-cache\r\n");
  buf.append("Cache-Control: no-cache\r\n");
  buf.append("Cache-Control: no-store\r\n");
  buf.append("Cache-Control: private\r\n");
}

Now we need only write these headers to the cache of headers used by NetDynamics. The project's onBeforeHeaderEvent is one good place to do this (if you want to write the cookie on every hit). For example:

public int this_onBeforeHttpHeaderEvent(CSpProjectHtmlEvent event) {
  CSpHttp.write(getSsidAsCookie());
  return PROCEED;
}

Note that despite what the NetDynamics 4.x API documentation tells you, the user's session information is fully known and accessible. You can both read and change session objects. (The same is true for onBeforeWebEvent.)

Note that these headers may be reset and overridden in some later event (by a call to CSpHttp.reset). We use an HTTP utility class to prevent this problem and to provide a more robust HTTP manipulation API. It is a practice I recommend. If there is interest perhaps the code can be distributed.

Parsing cookies in general:

Now that we have successfully stored the user's session identifier in a cookie, how do we retrieve it? First, let's look at parsing cookies in general. We can use a StringTokenizer to make the job easy. Here's snippet of one of the classes in the aforementioned custom API. It uses a Cookie class, which is essentially a struct for storing cookie data. The method assumes an input string that is the "value" part of the submitted Cookie HTTP header, and will return a Vector of our Cookie structs.

public class CookieParser {
  public static Vector getCookies(String httpcookiestr) {
    // create a vector to store the cookies...
    Vector vect = new Vector();

   // and a reference for the cookie we're building...
   Cookie curcookie = null;

   // and a holder for the default cookie version, since
   // this persists across all the cookies
   int version = 0;  // 0 indicates Netscape's specification

   // now, to get down to business, create a StringTokenizer to
   // separate the ";" delimited name/value pairs.
   StringTokenizer toker = new StringToker(httpcookiestr,";");

   // for each name/value pair...
   while(toker.hasMoreTokens()) {

      // get the nvp
      String nvp = toker.nextToken();

      // figure out where the equal sign is,
      // so that we can parse the name from the value
      int eq = nvp.indexOf("=");

      // if there's no equal sign, then skip this token
      if(-1 == eq) {
        break;
      } else {

        // otherwise, parse out name and value, 
        // trimming leading or trailing whitespace
        String name = nvp.substring(0,eq).trim();
        String value = nvp.substring(eq+1,nvp.length()).trim();

        // trim off leading or trailing quotes in the value, 
        // since RFC 2109 allows them
        if(value.startsWith("\"") && value.endsWith("\"")) {
          value = value.substring(1,value.length()-1));
        }

        // now check for the RFC 2109 reserved names
        if(name.startsWith("$")) {
          
           // we can encounter the $Version attribute outside
           // the scope of a cookie...
           if("$Version".equalsIgnoreCase()) {
              try { 
                 version = Integer.parseInt(value);
              } catch (NumberFormatException nfe) {
                 version = 0;
              } catch (NullPointerException npe) {
                 version = 0;
              }
           } else if(null != curcookie) {

             // ...but the other reserved names should only
             // appear within a cookie.  otherwise we'll
             // quietly ignore them 

             if("$Path".equalsIgnoreCase(name)) {
                curcookie.setPath(value);
             } else if("$Domain".equalsIgnoreCase(name)) {
                curcookie.setDomain(value);
             }
           
             // that's all RFC-2109 specifies
             // we will ignore any other "$name" cookies
             // or cookie attributes
          }
        } else {
             // otherwise, we must be starting a new cookie
             // since the name did not start with $
                               
             // add the old cookie, if it exists
             if(null != curcookie) {
               vect.addElement(curcookie);
             } 

             // and build a new one, with the name, value and version
             curcookie = new Cookie(name,value,version);

             // domain and path may be picked up later
        }             
      } // end of if(has equal sign)
    } // end of while hasMoreTokens
   
    // add the last cookie, if there was one
    if(null != curcookie) { vect.addElement(curcookie); }

    // and return
    return vect;
  } // end of getCookies

} // end of CookieParser

Re-entering the "cookied" session:

Suppose a request comes in without a SPIDERSESSION value, or with an expired or invalid SPIDERSESSION. Then the first framework method that gets triggered is the project's onNewSessionEvent. This is our first and best opportunity to reestablish the cookied session. The strategy here will be to only use the cookied value if no SPIDERSESSION value was passed. This means that an expired SPIDERSESSION from a cached page will cause a new session to be created, which is probably preferable in most situations.

/**
 * This flag indicates whether or not we've already
 * tried to establish an old session from the cookie.
 * We  need this to prevent an endless loop from occurring
 * when the cookied session id is invalid or expired, since
 * the call to getUserSessionInformation will trigger 
 * onNewSessionEvent in that case.
 * @see #this_onNewSessionEvent
 */
protected boolean _triedcookie = false;

public int this_onNewSessionEvent(CSpProjectSessionEvent event) {
  // first, make sure this is the first time thru the method
  if(!_triedcookie) {

     // check to see if SPIDERSESSION value was submitted
     if(null == CSpider.getWebVar("SPIDERSESSION")) {

        // no SPIDERSESSION var was passed, so this is either
        // a new user or we are returning from a non-ND page
        
        // try to get the ssid cookie
        Vector allcookies = CookieParser.getCookies(CSpider.getWebEnvVar(CSpVars.HTTP_COOKIE));

        // **assume the findCookie method returns the appropriate cookie
        // **we just need to find the corresponding name
        Cookie cookie = findCookie("ssid",allcookies);

        if(null == cookie) {
          // no cookie either, probably a brand new user, just PROCEED
          return PROCEED;
        } else {
          // found the cookie, try to deserialize it...
          
          // first, create a data input stream to deserialize from
          DataInputStream din = new DataInputStream(
             new ByteArrayInputStream(
               CSpHttp.descape(cookie.getValue()
             ).getBytes()
          );
          
          // second, create a new ssid to store the deserialized value
          CSpSessionId nussid = new CSpSessionId();
          
          // try to deserialize the data
          try {
             nussid.deserialize(din);
          } catch (IOException e) {
             CSpLog.send(this,CSpLog.ERROR,"this_onNewSessionEvent(): unable to "
                         + "deserialize session from cookie because " + e
                         + ". A new session will be created.");
             return PROCEED;
          }

          // we have successfully deserialized the ssid from the cookie
          
          // so remove the new session that ND has already created
          boolean ignored = CSpider.removeUserSession();

          // set _triedcookie to true, to avoid an endless loop
          _triedcookie = true;

          // reestablish the old session
          //   this is the method that may recursively trigger
          //   onNewSessionEvent
          int cmnd = CSpider.getUserSessionInformation(this,nussid);

          // set _triedcookie to false
          _triedcookie = false;

          return cmnd;          
        } // end if(cookie not null)
     } else {
        // else, we got a SPIDERSESSION from the web var,
        // it must be invalid or expired, so just proceed
        return PROCEED;
     }
   } else {
     // we've already tried to parse the session from the cookie,
     // the cookied session must be expired or invalid also
     // so just PROCEED
     // but first, set the _triedcookie flag to false
     _triedcookie = false;
     return PROCEED;
   }
} // end of this_onNewSessionEvent

You may want to tweak with the logic here to meet the needs of your application.

Problems with this technique:

There is at least one problem with technique that results in single license being thrown away for five minutes every time the session is restored from the cookie. It is best described as a narrative:

Suppose a user visits a our cookied-session application and gets a ssid cookie. Call that session C. The user then goes to a non-ND site and remains there for more than five minutes (so that we can ignore the first license the user occupied.)

Suppose the user then returns to the NetDynamics application without a SPIDERSESSION web variable (say, by following a link from the external application). Since this looks like a new user to the ND framework, ND will create a new session (call it session A) and trigger the onNewSessionEvent. Session A has been assigned a client ticket, and hence occupies a license for the next five minutes.

Now, we read the session id for session C from the cookie, and re-establish that. Processing continues as normal, and a page is written to the user's browser with the session C identifier contained in the SPIDERSESSION form element.

The user now submits that page to the application server. Since the session C does not have a license handle associated with it, a new one must be obtained. Hence there are now two license handles associated with the user.

What we're missing is a way to change the session identifier while maintaining the newly generated license handle. If you think about it, this may be by design, since if one were able to exchange session identifiers without exchanging license handles, one would be able to circumvent the ND licensing strategy by simply maintaining session identifier is another variable and pushing all the users through with the single license handle.


Care to comment on this article? Answer as many or as few as you choose, and click the send button.

Note that after submitting, you will be returned to this page. Don't worry, your comments have been recorded.

Overall, the article was...
The length of this article was...
The level of detail was...
You can enter any other comments you want in the field below.
Be sure to include your email address if you want a reply.

You are at: http://i.am/rwald / java / netdynamics / storing the netdynamics session id within a client-side "cookie"