HTTP Pipelines
Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET
Tim Ewald and Keith Brown
This article assumes you're familiar with ASP.NET and C#
Level of Difficulty
1
2
3
SUMMARY
ASP.NET is a flexible and extensible framework for server-side HTTP programming. While most people think of ASP.NET in terms of pages served, there is a lower-level infrastructure sitting beneath this page model. The underlying plumbing is based on a pipeline of app, module, and handler objects. Understanding how this pipeline works is key if you want to get the most out of ASP.NET as an HTTP server platform, while making your process more efficient, and keeping your server secure. This article introduces the architecture of the pipeline and shows how you can use it to add sophisticated functionality to an ASP.NET-based app.
Contents
Most people think of ASP.NET in terms of pages—that is, executable templates for creating HTML to return to browsers. But that is just one of many possible ways to use the ASP.NET core infrastructure, the HTTP pipeline. The pipeline is the general-purpose framework for server-side HTTP programming that serves as the foundation for ASP.NET pages as well as Web Services. To qualify as a serious ASP.NET developer, you must understand how the pipeline works. This article explains how the HTTP pipeline processes requests.
The Pipeline Object Model
The types defined in the System.Web namespace process HTTP requests using a pipeline model. The general structure of the pipeline is shown in
Figure 1. HTTP requests are passed to an instance of the HttpRuntime class, which represents the beginning of the pipe. The HttpRuntime object examines the request and figures out which application it was sent to (from the pipeline's perspective, a virtual directory is an application). Then it uses an HttpApplicationFactory to either find or create an HttpApplication object to process the request. An HttpApplication holds a collection of HTTP module objects, implementations of the IHttpModule interface. HTTP modules are filters that can examine and modify the contents of HTTP request and response messages as they pass through the pipeline. The HttpApplication object uses an HTTP handler factory to either find or create an HTTP handler object. HTTP handlers are endpoints for HTTP communication that process request messages and generate corresponding response messages. HTTP handlers and handler factories implement the IHttpHandler and IHttpHandlerFactory interfaces, respectively.
Figure 1
HTTP Pipeline Processing
An HttpApplication, its modules, and its handler will only be used to process one request at a time. If multiple requests targeting the same application arrive simultaneously, multiple HttpApplication objects will be used. The HttpApplicationFactory and HttpHandlerFactory classes pool HttpApplication objects and HTTP handler objects, respectively, for efficiency's sake.
The pipeline uses an HttpContext object to represent each request/response pair. The object is passed to the HttpApplication, which in turn passes it to the handler. Each module can access the current HttpContext as well. The HttpContext object exposes properties representing the HTTP request and response messages, which are instances of the HttpRequest and HttpResponse classes, respectively. The HttpContext object also exposes properties representing security information and per-call, per-session, and per-application state.
Figure 2 shows most of the interesting properties of the HttpContext class.
Figure 2 Interesting HttpContext Properties
Property |
Description |
Application |
Per-application cross-request state |
Application Instance |
Application object processing request |
Cache |
Per-application cached state |
Handler |
Handler object processing request |
Items |
Per-request state |
Request |
HTTP request message |
Response |
HTTP response message |
Server |
Utility functions |
Session |
Per-user cross-request state |
User |
User information |
The ASP.NET HTTP pipeline is fully extensible. You can implement your own HTTP modules, handlers, and handler factories. You can also extend the behavior of the HttpApplication class.
The Pipeline Process Model
The ASP.NET HTTP pipeline relies on Microsoft® Internet Information Services (IIS) to receive the requests it is going to process (it can also be integrated with other Web servers). When IIS receives an HTTP request, it examines the extension of the file identified by the target URL. If the file extension is associated with executable code, IIS invokes that code in order to process the request. Mappings from file extensions to pieces of executable code are recorded in the IIS metabase. When ASP.NET is installed, it adds entries to the metabase associating various standard file extensions, including .aspx and .asmx, with a library called aspnet_isapi.dll.
When IIS receives an HTTP request for one of these files, it invokes the code in aspnet_isapi.dll, which in turn funnels the request into the HTTP pipeline. Aspnet_isapi.dll uses a named pipe to forward the request from the IIS service where it runs, inetinfo.exe, to an instance of the ASP.NET worker process, aspnet_wp.exe. (In Windows® .NET Server, ASP.NET integrates with the IIS 6.0 kernel-mode HTTP listener, allowing requests to pass from the operating system directly to the worker process without passing through inetinfo.exe.) The worker process uses an instance of the HttpRuntime class to process the request.
Figure 3 illustrates the entire architecture.
Figure 3
ASP.NET Pipeline Architecture
The HTTP pipeline always processes requests in an instance of the worker processes. By default, there will only be one worker process in use at a time. (If your Web server has multiple CPUs, you can configure the pipeline to use multiple worker processes, one per CPU.) This is a notable change from native IIS, which uses multiple worker processes in order to isolate applications from one another. The pipeline's worker process achieves isolation by using AppDomains. You can think of an AppDomain as a lightweight process within a process. The pipeline sends all HTTP requests targeting the same virtual directory to a single AppDomain. In other words, each virtual directory is treated as a separate application. This is another notable change from native IIS, which allowed multiple virtual directories to be part of the same application.
ASP.NET supports recycling worker processes based on a number of criteria, including age, time spent idle, number of requests serviced, number of requests queued, and amount of physical memory consumed. The global .NET configuration file, machine.config, sets thresholds for these values (see the processModel element). When an instance of aspnet_wp.exe crosses one of these thresholds, aspnet_isapi.dll launches a new instance of the worker process and starts sending it requests. The old instance terminates when it finishes processing pending requests. Recycling of worker processes promotes reliability by killing off processes before their performance begins to degrade from resource leaks or other runtime phenomena.
HTTP Handlers
HTTP handlers are simply classes that implement the IHttpHandler interface, as shown in the following lines of code:
interface IHttpHandler
{
// called to process request and generate response
void ProcessRequest(HttpContext ctx);
// called to see if handler can be pooled
bool IsReuseable { get; }
}
Handlers can also implement the IHttpAsyncHandler interface if they want to support asynchronous invocation.
The ProcessRequest method is called by an HttpApplication object when it wants the handler to process the current HTTP request and to generate a response. The IsReuseable property is accessed in order to determine whether a handler can be used more than once.
The code in
Figure 4 implements a simple reusable HTTP handler that responds to all requests by returning the current time in an XML tag. You should note the use of the HttpContext object's Response property to set the response message's MIME type and to write out its content.
Figure 4 TimeHandler
using System;
using System.Web;
namespace Pipeline
{
public class TimeHandler : IHttpHandler
{
void ProcessRequest(HttpContext ctx)
{
ctx.Response.ContentType = "text/xml";
ctx.Response.Write("<now>");
ctx.Response.Write(
DateTime.Now.ToString());
ctx.Response.Write("</now>");
}
bool IsReuseable { get { return true; } }
}
}
Once an HTTP handler class is implemented, it must be deployed. Deployment involves three steps. First, you have to put the compiled code someplace where the ASP.NET worker process can find it. In general, that means you place your compiled .NET assembly (typically a DLL) in the bin subdirectory of your Web server's virtual directory or in the Global Assembly Cache (GAC).
Next, you have to tell the HTTP pipeline to execute your code when an HTTP request that meets some basic criteria arrives. You do this by adding an <httpHandlers> section to your virtual directory's Web.config file:
<configuration>
<system.web>
<httpHandlers>
<add verb="GET" path="*.time"
type="Pipeline.TimeHandler,
Pipeline"
/>
</httpHandlers>
</system.web>
</configuration>
This information is treated as an addendum to the configuration details specified in the global .NET machine.config file. In this example, the Web.config file tells the ASP.NET HTTP pipeline to process HTTP GET requests for .time files by invoking the Pipeline.TimeHandler class in the Pipeline assembly.
Finally, you have to tell IIS to route requests for .time files to the aspnet_isapi.dll library so that they can be funneled into the pipeline in the first place. This requires adding a new file mapping to the IIS metabase. The easiest way to do this is using the IIS management console, which shows a virtual directory's file extension mappings on the Mappings tab of the Application Configuration dialog (see
Figure 5).
Figure 5
Adding a File Mapping
In addition to implementing custom handlers, you can also write your own handler factories. A handler factory is a simple class that implements the IHttpHandlerFactory interface. Handler factories are deployed the same way handlers are; the only difference is that the entry in the Web.config file refers the factory class instead of the handler class that the factory instantiates. If you implement a custom HTTP handler without implementing a handler factory, an instance of the pipeline-provided default factory class, HandlerFactoryWrapper, is used instead.
Standard Handlers
The higher-level ASP.NET technologies, such as pages and Web Services, are built directly on top of the HTTP handlers. A quick peek at the global .NET machine.config file reveals the following <httpHandlers> entries:
<httpHandlers>
<add verb="*" path="*.ashx"
type="System.Web.UI.SimpleHandlerFactory"
/>
<add verb="*" path="*.aspx"
type="System.Web.UI.PageHandlerFactory"
/>
<add verb="*" path="*.asmx"
type="System.Web.Services.Protocols.
WebServiceHandlerFactory ... "
/>
</httpHandlers>
The first entry maps the .ashx file extension to the SimpleHandlerFactory class, an HTTP handler factory that knows how to compile and instantiate an implementation of IHttpHandler from the source code in an .ashx file. The resulting object can be used directly by the HTTP pipeline.
Figure 6 shows the TimeHandler example rewritten as an .ashx file. The @WebHandler directive tells the SimpleHandlerFactory the name of the HTTP handler class to instantiate after the source code has been compiled. The benefit of this approach is that it simplifies deployment: all you need to do is copy the .ashx file to your virtual directory. There is no need to create or modify a Web.config file or to update IIS—the requisite configuration was done when .NET was installed.
Figure 6 TimeHandler as an ashx File
<%@ WebHandler language="C#"
class="Pipeline.TimeHandler" %>
using System;
using System.Web;
namespace Pipeline
{
public class TimeHandler : IHttpHandler
{
void ProcessRequest(HttpContext ctx)
{
// set response message MIME type
ctx.Response.ContentType = "text/xml";
// write response message body
ctx.Response.Write("<now>");
ctx.Response.Write(
DateTime.Now.ToString());
ctx.Response.Write("</now>");
}
bool IsReuseable { get { return true; } }
}
}
The second <httpHandlers> entry maps the .aspx file extension to the PageHandlerFactory class, which is an HTTP handler factory that knows how to compile the source code in an .aspx file into a System.Web.UI.Page-derived class and instantiate it. The Page class implements the IHttpHandler interface, so the resulting object can be used directly by the HTTP pipeline.
The third entry maps the .asmx file extension to the WebServiceHandlerFactory class, which is an HTTP handler factory that knows how to compile the source code in an .asmx file into a class and instantiate it. Then it wraps the object with an instance of a standard HTTP handler (SyncSessionlessHandler by default) that uses reflection in order to translate SOAP messages into method invocations. Once again, the resulting object can be used directly by the HTTP pipeline.
It is important to note that the PageHandlerFactory, WebServiceHandlerFactory, and SimpleHandlerFactory classes do not compile .aspx, .asmx, and .ashx files on every request. Instead, the compiled code is cached in the Temporary ASP.NET Files subdirectory of the .NET installation directory. The code will only be recompiled when its corresponding source file changes.
HTTP Modules
HTTP handlers are endpoints for communication. Instances of handler classes consume HTTP requests and produce HTTP responses. HTTP modules are filters that process HTTP request and response messages as they pass through the pipeline, examining and possibly modifying their content. The pipeline uses HTTP modules to implement its own infrastructure, most notably security and session management.
HTTP modules are simply classes that implement the IHttpModule interface:
interface IHttpModule
{
// called to attach module to app events
void Init(HttpApplication app);
// called to clean up
void Dispose()
}
The Init method is called by an HttpApplication object when the module is first created. It gives the module the opportunity to attach one or more event handlers to the events exposed by the HttpApplication object.
The code in
Figure 7 implements a simple HTTP module that handles its HttpApplication object's BeginRequest and EndRequest events and measures the elapsed time between them. In this example, the Init method uses normal .NET techniques to attach the module's OnBeginRequest and OnEndRequest handlers to the events fired by the HttpApplication object the module is being attached to. The implementation of OnBeginRequest simply stores the time when the event fired in the member variable starts. The implementation of OnEndRequest measures the elapsed time since OnBeginRequest was called and adds that information to the response message using a custom HTTP header. The OnEndRequest method takes advantage of the fact that the first parameter passed to an event handler is a reference to the object that fired the event; in this case, it is the HttpApplication object that the module is attached to. The HttpApplication object exposes the Http-Context object for the current message exchange as a property, which is exactly how OnEndRequest is able to manipulate the HTTP response message.
Figure 7 Simple HTTP Module
using System;
using System.Web;
namespace Pipeline
{
public class ElapsedTimeModule : IHttpModule
{
DateTime start;
public void Init(HttpApplication app)
{
// register for pipeline events
app.BeginRequest +=
new EventHandler(this.OnBeginRequest);
app.EndRequest +=
new EventHandler(this.OnEndRequest);
}
public void Dispose() {}
public void OnBeginRequest(object o,
EventArgs args)
{
// record time when request started
start = DateTime.Now;
}
public void OnEndRequest(object o,
EventArgs args)
{
// measure elapsed time
TimeSpan elapsed =
DateTime.Now - start;
// get access to app and context
HttpApplication app =
(HttpApplication) o;
HttpContext ctx = app.Context;
// add custom header to HTTP response
ctx.Response.AppendHeader(
"ElapsedTime",
elapsed.ToString());
}
}
}
Once an HTTP module class is implemented, it must be deployed. Deployment involves two steps. As with an HTTP handler, you have to put the compiled module code either in the bin subdirectory of your Web server's virtual directory or in the GAC so that the ASP.NET worker process can find it.
Then you have to tell the HTTP pipeline to create your module whenever a new HttpApplication object is created to handle a request sent to your application. You do this by adding an <httpModules> section to your virtual directory's Web.config file, as shown here:
<configuration>
<system.web>
<httpModules>
<add
name="Elapsed"
type="Pipeline.ElapsedTimeModule, Pipeline"
/>
</httpModules>
</system.web>
</configuration>
In this example, the Web.config file tells the ASP.NET HTTP pipeline to attach an instance of the Pipeline.ElapsedTimeModule class to every HttpApplication object instantiated to service requests that target this virtual directory.
The Pipeline Event Model
The ElapsedTimeModule in the previous example implements two event handlers, OnBeginRequest and OnEndRequest. These are just two of the events that an HttpApplication object fires in the course of processing an HTTP message exchange. The complete list of events is shown in
Figure 8. Note that the HTTP handler object that an instance of the HttpApplication class uses to ultimately process a request message is created between the ResolveRequestCache and AcquireRequestState events. The user session state, if any, is acquired during the AcquireRequestState event. Finally, note that the handler is invoked between the PreRequestHandlerExecute and PostRequestHandlerExecute events.
Figure 8 Pipeline Events Fired by HttpApplication Objects
Event |
When It's Called |
BeginRequest |
Before request processing starts |
AuthenticateRequest |
To authenticate client |
AuthorizeRequest |
To perform access check |
ResolveRequestCache |
To get response from cache |
AcquireRequestState |
To load session state |
PreRequestHandlerExecute |
Before request sent to handler |
PostRequestHandlerExecute |
After request sent to handler |
ReleaseRequestState |
To store session state |
UpdateRequestCache |
To update response cache |
EndRequest |
After processing ends |
PreSendRequestHeaders |
Before buffered response headers sent |
PreSendRequestContent |
Before buffered response body sent |
The HttpApplication class exposes all these events using multicast delegates so that multiple HTTP modules can register for each one. HTTP modules can register event handlers for as many of their HttpApplication objects' events as they like. However, modules should register for as few events as possible for efficiency's sake. Since an HttpApplication object and its modules will only be used to process one HTTP request at a time, individual HTTP module objects can store any per-request state they need across multiple events.
In some cases, an HTTP module may want to influence the flow of processing in the pipeline. For example, a module that implements a security scheme might want to abort normal message processing and redirect the client to a login URL when it detects that the HTTP request message does not include a cookie identifying the user. The HttpApplication object exposes the CompleteRequest method. If an HTTP module's event handler calls HttpApplication.CompleteRequest, normal pipeline processing is interrupted after the current event completes (including the processing of any other registered event handlers). A module that terminates the normal processing of a message is expected to generate an appropriate HTTP response message.
The code in
Figure 9 provides an example of a module that uses CompleteRequest to abort the normal processing of a Web Service invocation. The SOAP specification's HTTP binding requires that an HTTP message carrying a SOAP message include a custom header called SOAPAction. The EnableWebServiceModule class's OnBeginRequest event handler examines the request message and if a SOAPAction header is present and the class's static enabled field is false, it stops further processing.
Figure 9 Stop Processing
using System;
using System.Web;
namespace Pipeline
{
public class EnableWebServicesModule :
IHttpModule
{
// field for turning behavior on and off
public static bool enabled = true;
public void Init(HttpApplication app)
{
// register event handler
app.BeginRequest +=
new EventHandler(this.OnBeginRequest);
}
public void Dispose() {}
public void OnBeginRequest(object obj, EventArgs ea)
{
// if web services are enabled, let
// request proceed through pipeline
lock(typeof(EnableWebServicesModule))
{
if (enabled) return;
}
// check to see if request is a SOAP
// message by looking for SOAPAction
HttpApplication app = (HttpApplication) obj;
HttpContext ctx = app.Context;
string s = ctx.Request.Headers["SOAPAction"];
if (s == null) return;
// if web services are disabled and
// request is SOAP message, abort processing
app.CompleteRequest();
ctx.Response.StatusCode = 403;
ctx.Response.StatusDescription = "Forbidden";
ctx.Response.ContentType = "text/plain";
ctx.Response.Write("No!");
}
}
Figure 10 contains the source code for an HTTP handler called EnableWebServicesHandler that toggles the EnableWebServiceModule class's static enabled field whenever its ProcessRequest method is invoked.
Figure 10 EnableWebServicesHandler
using System;
using System.Web;
namespace Pipeline
{
public class EnableWebServicesHandler :
IHttpHandler
{
public void ProcessRequest(HttpContext ctx)
{
// toggle module's enabled field and
// return new value as HTML
lock(typeof(EnableWebServicesModule))
{
EnableWebServicesModule.enabled =
!EnableWebServicesModule.enabled;
ctx.Response.ContentType = "text/html";
ctx.Response.Write("<h1>Web Services " +
(EnableWebServicesModule.enabled ?
"Enabled" : "Disabled") + "</h1>");
}
}
public bool IsReusable {get { return true; }}
}
}
Assuming that the source code for both the HTTP module and handler are compiled into a .NET assembly called Pipeline, the following entries in the Web.config file would be necessary for configuration:
<configuration>
<system.web>
<httpModules>
<add name="WebServicesEnabledModule"
type="Pipeline.EnableWebServicesModule, Pipeline"
/>
</httpModules>
<httpHandlers>
<add verb="*" path="toggle.switch"
type="Pipeline.EnableWebServicesHandler, Pipeline"
</httpHandlers>
</system.web>
</configuration>
An IIS metabase entry associating the .switch extension with aspnet_isapi.dll would also have to be created and the Pipeline assembly would have to be deployed in the bin subdirectory of the Web server's virtual directory or in the GAC.
There is one other important point to mention about HTTP modules and the HttpApplication.CompleteRequest method. If a module aborts normal message handling during an event handler by calling CompleteRequest, the ASP.NET HTTP pipeline interrupts processing after that event completes. However, EndRequest and the events that follow are still fired. Thus, any modules that acquired valuable resources before processing was terminated have a chance to clean up those resources. For instance, if a module acquired a lock against shared state in its BeginRequest event handler, it can release the lock in its EndRequest event handler and be confident that the right thing will happen. The EndRequest event will fire even if some other module calls CompleteRequest and the HTTP message is never delivered to a handler.
HTTP Applications
As we mentioned, the ASP.NET HTTP pipeline treats each virtual directory as an application. When a request for a URL in a given virtual directory arrives, the HttpRuntime object that dispatches the message uses an HttpApplicationFactory object to find or create an HttpApplication object to process the request. A given HttpApplication object will only be used to service requests sent to a single virtual directory, and there can be multiple pooled instances of HttpApplication for the same virtual directory.
If you want, you can customize the behavior of the HttpApplication class for your application (virtual directory). You do this by writing a global.asax file. If the HTTP pipeline detects a global.asax file in your virtual directory, it compiles it into an HttpApplication-derived class. Then it instantiates your specialized HttpApplication subclass and uses it to service requests.
The most interesting use of a global.asax file is to implement a subclass of HttpApplication that handles events fired by the HTTP pipeline. These events fall into two categories, one of which we've already discussed. First, you can use a global.asax file to implement handlers for events fired by an HttpApplication object itself (the events listed in
Figure 8). Normally HTTP modules handle these events, but in some cases implementing and deploying modules is not necessary—especially if the behavior you want to implement is application-specific.
For instance, you could implement a handler for the BeginRequest event in a global.asax file, as shown here:
<%@ import namespace="System.Web" %>
<!-- this code will be added to a new
HttpApplication-derived class -->
<script language="C#" runat=server>
public void Application_BeginRequest(
object obj, EventArgs ea)
{
string s = Context.Request.Headers["SOAPAction"];
Context.Items["IsSOAP"] = (s != null);
}
</script>
In this example, the Application_BeginRequest event handler detects the presence of a Web Service invocation based on the SOAPAction header. It records the fact that a SOAP invocation is being made in the Items property of the current HttpContext object (which is available as a property of the HttpApplication class, from which this code derives). The Items property is used to store a per-request state that you want to make available to multiple modules and the ultimate message handler. Its contents are flushed when a request completes. An HTTP module could provide this behavior, but if you only need it in one application it is simpler to implement it this way. (It might seem odd to have an HttpApplication-derived class handling events fired by its base class, which is what is happening here, but it is very convenient!)
Two additional application-level events are not listed in
Figure 8 and are not made available to HTTP modules in the normal way, namely, Application_OnStart and Application_OnEnd. These events are familiar to classic ASP programmers. They are called when an application is first accessed and when it shuts down, respectively. Here is a simple example:
<%@ import namespace="System.Web" %>
<script language="C#" runat=server>
public void Application_OnStart()
{
... // set up application here
}
public void Application_OnEnd()
{
... // clean up application here
}
</script>
The other category of events that an HttpApplication-derived class might handle is events fired by HTTP modules. In fact, this is how the pipeline implements the classic ASP Session_OnStart and Session_OnEnd events, both of which are fired by the SessionStateModule class.
Consider the EnableWebServicesModule presented earlier that conditionally rejects Web Service invocations based on the state of a static field. When it rejects a request, it does so brusquely, with a hardcoded, somewhat curt message. It might be better if the module allowed the application it is being used with to tailor the message for its own purposes. One way to do this is to have the HTTP module fire an event when a Web Service request is rejected.
Figure 11 shows a new version of the EnableWebServicesModule that fires a Rejection event when a Web Service request is rejected. The modified implementation of the module's OnBeginRequest event handler checks to see if there are any handlers registered for the Rejection event by comparing the property to null. If one is registered, the module fires the event and expects the handler to produce an appropriate HTTP response message. If no handlers are registered, the module generates its own HTTP response message with the same abrupt tone.
Figure 11 Firing a Rejection Event
using System;
using System.Web;
namespace Pipeline
{
public class EnableWebServicesModule :
IHttpModule
{
// event for callback to application
public event EventHandler Rejection;
// field for turning behavior on and off
public static bool enabled = true;
public void Init(HttpApplication app)
{
// register event handler
app.BeginRequest +=
new EventHandler(this.OnBeginRequest);
}
public void Dispose() {}
public void OnBeginRequest(object obj, EventArgs ea)
{
// if web services are enabled, let
// request proceed through pipeline
lock(typeof(EnableWebServicesModule))
{
if (enabled) return;
}
// check to see if request is a SOAP
// message by looking for SOAPAction
HttpApplication app = (HttpApplication) obj;
HttpContext ctx = app.Context;
string s = ctx.Request.Headers["SOAPAction"];
if (s == null) return;
// if web services are disabled and
// request is SOAP message, abort
// processing
app.CompleteRequest();
// if application registered for event
// fire it and let handler generate
// HTTP response, otherwise issue
// own blunt response
if (Rejection != null)
{
Rejection(this, EventArgs.Empty);
}
else
{
ctx.Response.StatusCode = 403;
ctx.Response.StatusDescription = "Forbidden";
ctx.Response.ContentType = "text/plain";
ctx.Response.Write("No!");
}
}
}
}
An application can handle the events fired by a module simply by implementing a method with the correct signature. The syntax is based on the name assigned to the HTTP module in the Web.config file when the module was deployed and the name of the event the module fires. In the previous example, the module was given the name EnableWebServicesModule (which also happens to be its class name, but that is just coincidence) and the event is called Rejection. Based on that, the signature for the HttpApplication subclass's handler for the event is:
public void EnableWebServicesModule_Rejection(
object o, EventsArgs ea);
Here is an implementation:
<%@ import namespace="System.Web" %>
<script language="C#" runat=server>
public void EnableWebServicesModule_Rejection(
object o, EventArgs ea)
{
Context.Response.StatusCode = 403;
ctx.Response.StatusDescription = "Forbidden";
ctx.Response.ContentType = "text/plain";
ctx.Response.Write("Unfortunately, web " +
"services are not available now, " +
"please try your request again");
}
</script>
The pipeline plumbing knows how to wire up this event handler based on its name. Now when the HTTP module rejects Web Service invocations, it will fire the Rejection event and the application will have a chance to generate a friendlier HTTP response message. The entire new architecture, including the handler for controlling the module's behavior, is shown in
Figure 12.
Figure 12
EnableWebServicesModule Architecture
Security in the Pipeline
One of the most common uses of HttpModules is to implement security features such as authentication and authorization, a healthy dose of which can be layered on top of an application quite transparently. In fact, take a look at the default list of HttpModules installed for all Web applications by machine.config (We've omitted the type names for brevity):
<httpModules>
<add name="OutputCache" type="..."/>
<add name="Session" type="..."/>
<add name="WindowsAuthentication" type="..."/>
<add name="FormsAuthentication" type="..."/>
<add name="PassportAuthentication" type="..."/>
<add name="UrlAuthorization" type="..."/>
<add name="FileAuthorization" type="..."/>
</httpModules>
Aside from the output caching and session state management modules, these modules are there to help implement security. Also note that the order of the modules is important. Authentication answers the question "Who are you?", while authorization answers the question "Are you allowed to do this?" Clearly authentication must happen before authorization, thus the order of the modules shown previously.
The three authentication modules correspond to the three options in web.config for performing authentication:
<authentication mode='None|Windows|Forms|Passport'>
By selecting a mode other than None, you enable the corresponding authentication module to do its work. The job of these modules is to perform an authentication handshake with the client and possibly a trusted authority such as passport.com. Once authenticated, these modules create an implementation of IIdentity and IPrincipal that can be used by the authorization modules downstream to determine if the request should be granted or denied. This information is hung on the HttpContext.User property. The HttpHandler at the end of the pipeline can also use this information.
To illustrate, imagine your web.config file was written this way:
<configuration>
<web.config>
<authentication mode='Forms'/>
<authorization>
<deny users='?'/>
<allow roles='Managers, Staff'/>
<deny users='*'/>
</authorization>
</web.config>
</configuration>
Now imagine that a user tries to access the Web application, but is not currently logged on via Forms authentication and thus is considered anonymous. The FormsAuthentication module first processes the request and notices that the client has not sent the special cookie that represents a successful prior login. Thus it constructs an IIdentity object that indicates the user is anonymous and an IPrincipal object that binds an empty set of roles to that identity, attaching this to the HttpContext.User property. When the UrlAuthorization module gets the request, it notes that anonymous access to the directory has been denied in web.config. The <deny users='?'/> tag in web.config represents denial of any anonymous requests. The module checks the user associated with the current context via the HttpContext.User property, sees that it's anonymous, and therefore completes the request and indicates that access is forbidden. Remember though, we're not done yet. The FormsAuthenticationModule now gets to see this forbidden request being sent back to the client, notes that the user is not authenticated, and therefore changes the response into a redirect to the default login page, login.aspx.
Assuming the user submits valid credentials to the login page, the login page handler calls FormsAuthentication.RedirectFromLoginPage, which redirects the user back to the page she was after in the first place while sending her an encrypted cookie containing the authenticated user's name. When the redirection causes the client to request the original page again, the FormsAuthentication module decrypts and validates the cookie, then constructs an IIdentity for an authenticated user along with her name. This identity is bound with an empty set of roles and attached to HttpContext.User.
We didn't mention this before, but after setting HttpContext.User, the FormsAuthentication module causes the AuthenticateRequest event to fire. If you've implemented a handler for this event in your global.asax file, you'll now have a chance to take the IIdentity produced by the FormsAuthentication module and bind it to a set of application-defined roles. The code in
Figure 13 shows an example of this.
Figure 13 AuthenticateRequest Handler
<%@application language='C#'%>
<script runat=server>
public void Application_AuthenticateRequest(
object obj, EventArgs ea)
{
IPrincipal originalPrincipal = Context.User;
IIdentity identity = originalPrincipal.Identity;
if (identity.IsAuthenticated)
{
string[] roles = lookupRoles(identity.Name);
Context.User = new GenericPrincipal(identity, roles);
}
}
string[] lookupRoles(string userName)
{
// database lookup omitted for brevity
}
</script>
Now that the FormsAuthentication module is finished processing the request, the UrlAuthorization module has its turn. It notes that the request is authenticated, so the first line in the <authorization> section of web.config is satisfied. Now it looks to see if the principal is in either the Managers or Staff role. If so, the request will be allowed; otherwise, the last line of the <authorization> section will cause the request to be denied. In the <authorization> section of web.config, the wildcard character (?) indicates unauthenticated requests; the star character (*) indicates all requests.
This example illustrates how the flexibility of the HTTP pipeline makes it possible to layer security—specifically authentication and authorization—onto many different Web applications without much effort.
Conclusion
This article introduced the ASP.NET pipeline, a very flexible infrastructure for server-side HTTP development. The HTTP pipeline integrates with IIS and provides a rich programming model based on applications, modules, and handlers—all of which you can implement if you want. The HTTP pipeline is a large piece of plumbing and there are many important aspects to it that we did not have space to mention, including support for state management, which is a feature-length topic in its own right. Hopefully this article will help you better understand how the pipeline works and how you can use it in your HTTP-based .NET applications.
For background information see:
HttpApplication Members
HttpApplication Methods
Tim Ewaldis a Program Manager for XML Web Services at Microsoft, working on Web Service specifications, APIs, and distributing information to developers. He is the author of
Transactional COM+:
Building Scalable Applications (Addison-Wesley, 2001). Reach Tim at
[email protected].
Keith Brownworks at DevelopMentor researching, writing, teaching, and promoting an awareness of security among programmers. Keith authored
Programming Windows Security (Addison-Wesley, 2000). He coauthored
Effective COM, and is currently working on a .NET security book. He can be reached at
http://www.develop.com/kbrown