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
tewald@microsoft.com.
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.