August 30, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Enabling POST in Cookieless ASP.NET Applications

  • June 3, 2003
  • By Tomasz Kaszuba
  • Send Email »
  • More Articles »

The reasons could be varied why you'd want to eliminate cookies from your applications: security, maintenance, ethical, and so forth. Whatever the reason may be, like me, you have chosen (or have been forced) to write a cookieless Web application.

Microsoft has offered a solution in the form of URL rewriting to maintain session state in cookieless Web applications. At first sight, this seems like the ideal solution: Change one line in web.config and suddenly your cookie Web app magically becomes a cookieless Web app. Unfortunately, not everything that shines is gold. When choosing a cookieless Web app, be prepared to give up some of the benefits that a cookie-managed application provides. Things such as Form-based authentication, neat/custom URLs, problems with mobile applications, or the ability to POST data from an HTML page become a thing of the past. It's too bad that Microsoft didn't find more time to fill some of these concerns, but with a little bit of imagination there's a way to work around all these limitations.

I won't go into detail on overcoming the form-based authentication problems because I believe there's enough information already available on the Web. (Check the asp.net newsgroup on Google) With enough interest in the second problem, I can be persuaded to write a workaround on how to create custom URLs (eliminate the session string or move the session string). The third problem is covered on MSDN. The last problem is probably the most frustrating problem associated with cookieless sessions because it takes away one of the basic mechanisms a Web developer has to share data between HTML pages and Web forms.

The problem manifests itsself when posting data from an HTML page and trying to access the data in the Request.Form collection, which despite your best efforts, always seems to be empty. When using IE, it becomes very frustrating to find what actually happens to the posted data. Everything seems to work the same as in cookie managed sessions. My previous statement of Asp.Net erasing the posted data is a bit misleading. To find out what's really happening, it's worthwhile to run the Web app in a different browser.

If you run the app with Opera 6.01 (later versions I suspect function the same way) you'll notice the "Object has moved here" Web page. More unwanted behavior in cookieless sessions. The page requires user intervention to redirect the page. The page is also in English, which might be a show stopper for an international Web app. Getting back to what's actually happening; Asp.Net intercepts the posted request, adds the session id, and redirects it to the same page. In IE the redirection is invisible to the user because it supports the redirect request in the HTTP header. In the case of Opera 6.01, it doesn't; hence the "Object has moved here" communication. Of course, this process is only necessary once; after the session id is in the URL, there is no need to insert it again. This is why Web form pages behave normally when posting data to themselves or to other web forms. Of course, if they didn't, ViewState couldn't be maintained and server-side events would be useless.

The downside of redirecting a request in this manner is that it's always as a GET request. This turns out to be the answer to our problem. Our POST request suddenly becomes a GET, making transferring of our posted data impossible. We could work around this problem by attaching our posted data to the query string. But, if we don't mind our posted data in the URL, we might as well forego the hassle of posting the data in the first place and automatically form a GET request in our HTML page. By far, this is the easiest solution to the cookieless post problem and if you don't need the functionality offered by posting your data then this is the solution for you. Reading to the end of this article will be for purely educational purposes only.

For the rest of us, it's clear that to resolve this problem we have to find out when Asp.Net redirects the response, clear it, and once again post our data with the session id string. Our first clue is found in the statement that the session id string is inserted only once, so the first place to look is where the session is set up. The entire application uses the same mechanism to set up sessions, so it must be before our custom page receives the request and therefore global to our application.

Asp.Net provides a mechanism for implementing global methods by implementing the IHttpHandler interface. I won't go into the details of what an IHttpHandler is because I believe that MSDN does a great job of explaining the details. I'll just summarize that the IHttpHandler allows the programmer to capture and handle events thrown by an instance of the HttpApplication object that processes all incoming requests, in essence, the controller in an MVC architecture.

The IHttpHandler interface requires that we implement one method, public void Init (HttpApplication application). HttpApplication throws a whole array of events at different points in the lifetime of processing a page. In the beginning, let's register a handler for setting up session state.

application.AcquireRequestState
      += new EventHandler(this.Application_AcquiredSession);

Now, in our handler we can see whether our posted data is still available:

private void Application_AcquiredSession(Object source,
                                         EventArgs e)
{
  HttpRequest req = HttpContext.Current.Request;
  string reqtype = req.RequestType;	
}

I've used a call to HttpContext.Current instead of casting the source object to an HttpApplication. In our module, it wouldn't make much difference, but in other modules the event might be thrown by something different then an HttpApplication, so the call to HttpContext.Current will always be safe.

If we now run our debugger, we can see that the request is already a GET request. The request is redirected before the session is acquired. This makes it a little bit difficult to make an all-purpose module because we can't use Session.IsCookieless to detect whether the session is cookieless. Our only alternative is to read the web.config file directly. I haven't done much research into this subject, but I'm sure there's a solution on how to read elements from the web.config file differing from the <appsettings> element.

Going back to our init event, we'll remove the application.AcquiredRequestState handler; that leaves us either the BeginRequest or the EndRequest event. To save ourselves some time, I'll mention that both the events have access to the posted data. My initial reaction was to implement a custom Response Filter in the BeginRequest event and capture the moment when ASP.Net writes the headers and the Web page into the output stream. From a performance point of view, this would be the ideal solution because we wouldn't have to write the content/headers twice, but unfortunately the data gets written before the BeginRequest event. It makes sense to use the EndRequest event over the BeginRequest event because the EndRequest event will guarantee that all data and headers have been written into the Response object and hence our redirect won't be overwritten. In our event handler, we can now try to clear the headers and the content.

private void Application_EndRequest(Object source, EventArgs e)
{
  HttpRequest req = HttpContext.Current.Request;
  HttpResponse res = HttpContext.Current.Response;
  res.ClearContent();
  res.ClearHeaders();
  res.Output.Flush();
}

If we run our code now, we'll get a blank page, which is exactly what we should get because we've cleared all the output. This clears the way for adding our own custom redirection.

private string ConstructPostRedirection(HttpRequest req,
                                        HttpResponse res)
{
  StringBuilder build = new StringBuilder();
  build.Append(
   "<html>\n<body>\n<form name='Redirect'
                          method='post' action='");
  build.Append(res.ApplyAppPathModifier(req.Url.PathAndQuery));
  build.Append("' id='Redirect' >");
  foreach (object obj in req.Form)
  {
     build.Append(string.Format(
          "\n<input type='hidden' name='{0}' value = '{1}'>",
          (string)obj,req.Form[(string)obj]));
  }
  build.Append(
    "\n<noscript><h2>Object moved <input type='submit'
                                   value='here'></h2></noscript>");
  build.Append(@"</form>
  <script language='javascript'>
  <!--
    document.Redirect.submit();
  // -->
  </script>
  ");
  build.Append("</body></html>");
  return build.ToString();
}

There's quite a bit going on here. We construct our new <form> with all our posted data and automatically post it with JavaScript (in exactly the same way that ASP.Net does it). We provide a <noscript> tag for browsers that don't support JavaScript or have it turned off and still want to be redirected. This makes a great place to put international messages from satellite assemblies, informing users about having JavaScript-enabled browsers or text about redirection.

Another important statement is the call to HttpRequest.ApplyAppPathModifier. This method returns the path modified with the inserted session id.

We can now add our custom redirection to our output stream:

private void Application_EndRequest(Object source, EventArgs e)
{
  HttpRequest req = HttpContext.Current.Request;
  HttpResponse res = HttpContext.Current.Response;
  res.ClearContent();
  res.ClearHeaders();
  res.Output.Flush();
  char[] chr = ConstructPostRedirection(req,res).ToCharArray();
  res.Write(chr,0,chr.Length);
}

This will certainly redirect our page but it will also hang our application because our page will keep redirecting itself indefinitely, which is not good. We need to do a series of checks to see whether the request is a postback and whether the session is already set up. We don't need to keep inserting the session id on every request.

The finished module is as follows:

using System;
using System.Collections.Specialized;
using System.Web;
using System.Web.SessionState;
using System.IO;
using System.Text;

namespace CustomModule
{

  public sealed class CookielessPostFixModule : IHttpModule
  {
    public void Init (HttpApplication application)
    {
      application.EndRequest += new
                  EventHandler(this.Application_EndRequest);
    }
    private string ConstructPostRedirection(HttpRequest req,
                                            HttpResponse res)
    {
      StringBuilder build = new StringBuilder();
      build.Append(
  "<html>\n<body>\n<form name='Redirect' method='post' action='");
      build.Append(res.ApplyAppPathModifier(req.Url.PathAndQuery));
      build.Append("' id='Redirect' >");
      foreach (object obj in req.Form)
      {
        build.Append(string.Format(
  "\n<input type='hidden' name='{0}' value = '{1}'>",
          (string)obj,req.Form[(string)obj]));
      }
      build.Append(
  "\n<noscript><h2>Object moved <input type='submit'
                                       value='here'></h2>
                                       </noscript>");
      build.Append(@"</form>
      <script language='javascript'>
      <!--
      document.Redirect.submit();
      // -->
      </script>
      ");
      build.Append("</body></html>");
      return build.ToString();
    }
    private bool IsSessionAcquired
    {
      get
      {
        return HttpContext.Current.Items.Count>0;
      }
    }
    private string ConstructPathAndQuery(string[] segments)
    {
      StringBuilder build = new StringBuilder(); 

      for (int i=0;i<segments.Length;i++)
      {
        if (!segments[i].StartsWith("(") 
                 && !segments[i].EndsWith(")"))
          build.Append(segments[i]);
      }
      return build.ToString();
    }
    private bool IsCallingSelf(Uri referer,Uri newpage)
    {
      string refpathandquery = ConstructPathAndQuery(
                                        referer.Segments);
      return refpathandquery == newpage.PathAndQuery;
    }
    private bool ShouldRedirect
    {
      get
      {
        HttpRequest req = HttpContext.Current.Request;

        return (!IsSessionAcquired
                    && req.RequestType.ToUpper() == "POST"
          && !IsCallingSelf(req.UrlReferrer,req.Url));
      }
    }
    private void Application_EndRequest(Object source, EventArgs e)
    {
      HttpRequest req = HttpContext.Current.Request;
      HttpResponse res = HttpContext.Current.Response;
      if (!ShouldRedirect) return;
      res.ClearContent();
      res.ClearHeaders();
      res.Output.Flush();
      char[] chr = ConstructPostRedirection(req,res).ToCharArray();
      res.Write(chr,0,chr.Length);
    }
    public void Dispose()
    {}
  }
}

The call to ShouldRedirect checks to see whether the session is acquired, the type of request, and whether the request is a post back. If we wanted to, we could take out the RequestType call and eliminate GET requests from our Web app all together. Such a move could be useful if we wanted to clean the URL string.

The ConstructPathAndQuery method is a custom implementation used to construct the Path and Query without the session id, needed to check whether the page is calling itself. The Page object is not yet created, so we need to create our own function to fulfill the role of IsPostBack.

A quick note on thread safety. There's some discrepency on how Asp.Net goes about processing IHttpModules. MSDN tells us that each request gets a unique instance of an IHttpModule. After processing, the module goes back into a pool to be effectively recycled. If we follow this approach, the use of class level variables should be okay as long as we reinitialize them after we finish processing the request (or reinitialize them at the beginning if you're extra careful). This is why we have to implement the IDisposable interface. In response to that, I have read articles claiming that you shouldn't use class-level variables at all because each module could be handling many requests at once. If this happens to be the case, we can assume that only the EventHandlers are thread safe and the lock is maintained by HttpApplication. I'm leaning towards the first case, but if I'm wrong, it'd be nice if somebody set me straight. Just to be safe, I've intentionally left out class-level variables in our custom module and replaced them by private properties.

The last step is to register our custom module in web.config. In the system.web section, we add:

  <httpModules>
    <add type="CookielessPostFixModule, OurModule"
               name="CookielessPostFixModule" />
  </httpModules>

That's all there is to it. This module now takes care of redirecting our posted data.

# # #

Tomasz Kaszuba is currently a senior Web developer for Poland's biggest online bank, Inteligo. He possesses over 6 years of programming experience in Java and currently .NET.




Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel