Playing with HttpModules, HttpResponse.Filters and Streams in .NET
Previously, I created a little Html extension to help with skinning a website
based on the subdomain. It is working out for me pretty well in my current
project, but I wanted something that wouldn’t be so noisy in my source files.
My idea was that I could place a token in my source files and then when I am
about to return the result to the client I can replace the token with the path
to my skin directory. This also has the advantage of working in static files
like Html, Css and JavaScript.
1 |
<link href="|SKIN_DIR|Site.css" type="text/css" rel="stylesheet"/>
|
The problem I ran into is when working with
blocks that at some point the urls contained within that block are expanded
and escaped. Resulting in paths that look like…
1 |
<link rel="stylesheet" href="Views/Home/%7CSKIN_DIR%7CSite.css" type="text/css" />
|
Not only that, but the syntax highlighter definitely didn’t care for the
|SKIN_DIR| token from appearing in the href path.
And the Css files didn’t like the escape sequence either.
1 |
body { |
2 |
background-image: url("|SKIN_DIR|nebula.jpg");
|
3 |
} |
Getting turned into this mess.
1 |
body { |
2 |
background-image: url("http://localhost:1038/content/default/site.css/Content/default/nebula.jpg");
|
3 |
} |
I don’t even know what was going on there. Somehow the full Url for the
stylesheet got inserted and then the root path for the image resource got
expanded. Unexpected.
I tried a few variation on the event the sequence of the HttpModule’s
event sequence to try to capture the file before it was modified by the runtime
but with little success. Until I stumbled upon something by accident.
1 |
<link rel="stylesheet" href="http://localhost:1038/Content/default/Site.css" type="text/css" />
|
Well, I’ll be a monkey’s uncle. What in the world allowed *that* to work
but escaped everything else? Comments.
1 |
<link href="/*SKIN_DIR*/Site.css" type="text/css" rel="stylesheet"/>
|
At this point I’m not entirely sure what happened here but here’s my guess.
There’s a scrubber that cleans up and escapes the Html in the response code. And
the | (pipe symbol) get’s treated as an invalid character and is
thereby escaped. I think the scrubber erroneously treats the inline
/* */ (block comments) as valid escape codes and ignores them. Then
when the response stream reaches my filter it arrives intact. Allowing me to
swap out the token for my replacement. This even works for the Css files.
Despite some unexpected behavior and a lingering question as to exactly why
this apparently strange result occurred, I’m pleased with how easy it is to
intercept and manipulate your response stream in ASP.NET.
You can intercept the response in two ways. Either from within your
global application event or with a HttpModule. Both use the same
technique to handle the actual processing. They only differ in the level of
flexibility, where the handler get’s invokes, and a minor registration in your
Web.config.
Either way you approach this you’re going to need to implement a override the
Stream object.
using System.IO;
using System.Text;
namespace MvcApplication1.Code
{
public class Process : Stream
{
readonly Stream _processingStream;
readonly string _skinDir;
readonly string _skinDirToken;
public Process(Stream processingStream, string skinDirToken, string skinDir)
{
_processingStream = processingStream;
_skinDirToken = skinDirToken;
_skinDir = skinDir;
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { return 0; }
}
public override long Position { get; set; }
public override void Flush()
{
_processingStream.Flush();
}
public override long Seek(long offset, SeekOrigin origin)
{
return _processingStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_processingStream.SetLength(value);
}
public override int Read(byte[] buffer, int offset, int count)
{
return _processingStream.Read(buffer, offset, count);
}
// This is where the magic happens.
// The Write method is where the Response filter will pass in the stream
// that you can process.
public override void Write(byte[] buffer, int offset, int count)
{
var original = Encoding.Default.GetString(buffer);
var processed = ReplaceToken(original, _skinDirToken, _skinDir);
buffer = Encoding.Default.GetBytes(processed);
_processingStream.Write(buffer, 0, buffer.Length);
}
public string ReplaceToken(string original, string skinDirToken, string skinDir)
{
return original.Replace(skinDirToken, skinDir);
}
}
}
Now that you have a custom implementation of the Stream class
you can wire it up to a Response.Filter to process the stream.
If you choose just to wire it up to the global application event you can
simply drop wire up the filter in your Global.asax.
protected void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var context = application.Context;
var uri = new UriBuilder(context.Request.Url) {Host = context.Request.Headers["HOST"].Split(':')[0]};
uri.Path = Path.Combine(Path.Combine(uri.Path, "Content"), SkinName());
var skinDir = string.Format("{0}/", uri);
context.Response.Filter = new Process(context.Response.Filter,
ConfigurationManager.AppSettings["SkinDirToken"],
skinDir);
}
If you’d rather register the filter as an HttpModule then the
body of the method is the same. And you may even notice that the event is
also the same.
public class ProcessingModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.PreRequestHandlerExecute += context_PreRequestHandlerExecute;
}
public void Dispose()
{
}
void context_PreRequestHandlerExecute(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var context = application.Context;
var uri = new UriBuilder(context.Request.Url) {Host = context.Request.Headers["HOST"].Split(':')[0]};
uri.Path = Path.Combine(Path.Combine(uri.Path, "Content"), SkinName());
var skinDir = string.Format("{0}/", uri);
context.Response.Filter = new Process(context.Response.Filter,
ConfigurationManager.AppSettings["SkinDirToken"],
skinDir);
}
}
You’ll also need to register the module in your Web.config.
1 |
<httpModules>
|
2 |
<add name="SkinProcessing" type="MvcApplication1.Modules.ProcessingModule"/>
|
3 |
</httpModules>
|
It’s very easy to touch and manipulate your ASP.NET response with hardly
any difficulty. Although, I’ll be studying up on my ASP.NET pipeline to
figure out where that darn code is being escaped.
Just for reference, here’s the SkinName method. In my actual
demo code I’ve refactored to use a processing service and wire up the
implementation using StructureMap. That’s another post though.
public static string SkinName()
{
var context = (new HttpContextWrapper(HttpContext.Current));
var skinName = context.Request.Headers["HOST"].Split('.')[0].Split(':')[0];
return 0 == string.Compare(skinName, context.Request.Url.Host.Split(':')[0]) ? "default" : skinName;
}
PS. This entire post was composed within 30 minutes. This “do it on the train”
thing is a real incentive. So please forgive any glaring typos or poor
spelling. I’m in love with e-TextEditor, but it lacks a spell checker that
I know of.
PPS. Thanks to @rrinaldi for the motivation yesterday. It went through to
today as well. :)