I prefer "conservation of energy" myself.
Last time, we looked at why I would want to do such a crazy thing as serving files from the App_Data directory, when asp.net already forbids it for security reasons. We even looked at a rather naive approach for doing it, and saw that it was missing some rather important features - namely efficient memory usage, mime types and conditional gets.
This time, we'll get someone else to do all the heavy lifting for us. We're after a http handler that can serve static files. Asp.net has StaticFileHandler. Isn't that handy?
StaticFileHandler is how you can serve html, css, gif and jpg files when you route all files through asp.net. It pretty much does everything we want it to - it uses HttpResponse.WriteFile for small files and HttpResponse.TransmitFile for large files. It uses the undocumented and internal System.Web.MimeMapping class to map between an extension and it's mime type (hard coded, surprisingly enough) and most of all, it returns etags. It even ensures the file exists and you've got permissions and has code to handle resuming broken downloads. We've hit the jackpot. Almost.
So how can we use this? I was going to present a code sample here, but that "almost" above is stopping me. This isn't the post I set out to write - I can't advocate this method (which is a shame, 'cos I liked it - it was simple, looked like it worked, and was only slightly hacky). But i'm going to tell you about it anyway.
I created a class I called RewritingStaticFileHandler. It had a static constructor, which created an instance of StaticFileHandler and stashed it away (bizarrely, StaticFileHandler is internal, even though it's used in the default machine.config. Since it's in that file, I reckon it's fair game, so spin one up by reflection. This should have been a warning sign that things weren't going to work out too clever).
The ProcessRequest method of my new http handler called HttpContext.RewritePath, and passed in a new path in the App_Data folder with the file part coming from a query string parameter (danger Will Robinson!). It then called ProcessRequest on the stashed StaticFileHandler and voila - we're now serving files from App_Data, returning an etag, populating the mime data and I've only written 5 or 6 lines. Lovely. Almost.
Anyone spot the security hole? I'm setting the rewrite path to "~/App_Data/Images/" + Context.Request.QueryString["imageId"]. Pass in ../password.txt and it's going to serve the password.txt file from the App_Data directory. Not good.
This is quite straightforward to fix:
private void EnsureCanonicalPath(string path)
{
string mappedPath = Context.Server.MapPath(path);
if (Path.GetFullPath(mappedPath) != mappedPath ||
!mappedPath.StartsWith(Context.Server.MapPath("~/App_Data/Images/"));)
{
throw new HttpException(404, "Not found.");
}
}
Turn the path into a physical path, call GetFullPath (which always returns a canonical path) and ensure it starts with the physical path of the images directory. This way you're ensuring that you only serve files from the images subdirectory.
So why am I not happy with this approach? Well, it's a little bit hacky using reflection, but that's not it. I fired up Reflector to check what StaticFileHandler did under the covers, and I don't like what I see. It has a number of bugs. Where do I start?
There is a method called SendEntireEntity, which returns a bool. This is the only time the incoming etag header is checked. If you're requesting a range, this method checks the etag header and if etags match, returns false - send a range. Otherwise it returns true. The return value is ignored and it always sends back the entire file.
Any conditional get headers, such as If-Unmodified-Since, Unless-Modified-Since, etc are not checked, so the etag is useless there.
The etag value itself is derived from the last modified time of the file concatenated with the current time. This is generated at the start of each request. So, if you are asking for a range, you'll pass in an etag you received when you started downloading the file. This etag will include the time of the request. The new request will get a new etag. So the etags are always different. Again, useless.
So why did it look like it was working? For small files, it sends back a cache expiry date for a day hence, so IE just wasn't bothering to request the file again. But when it will request the file, the handler will just return it in full.
I'm not very happy about this. I was expecting StaticFileHandler to be a bit better than this. Having this kind of problem has lots of implications - I'm going to have to make some changes at work based on this. Bah. Not the post I wanted to write. Still, it means there's another one to add to the queue - BetterStaticFileHandler.
And where does that leave serving files from App_Data? Well, this solution will still work, it's just not the most efficient - definitely not something I'd recommend in an enterprise, but probably not too much of a problem for a blog. It's also not the nicest solution, so let's have a look if there's anything more elegant. Let's have a look at Virtual Path Providers.