I’m spending sometime this evening doing some maintenance on my main Web site. My main site – the static part of it anyway – uses only minimal ASP.NET code. Most of the site still runs classic ASP pages because there’s very little dynamic stuff going on on the home page or the various product and informational pages.
As of recent though I’ve been slowly adding more and more functionality in various places. Tonight I built a small handler that lets all download files that are available in EXE format be automatically available as a Zip file so that those folks that get blocked from downloading EXE files can get zipped files to download safely (a silly rule if you ask me – but…)
The module was trivial to build – I used #SharpZip for the file compression to write out a zip file to disk and the module then compares timestamps to only zip the file once and when the file has changed always serving the file with TransmitFile. Piece of cake. To build the module I created a temporary directory/site and worked off that. All’s well there.
Unfortunately I’ve not been able to install this module on my full site though. My plan was to install the module in the root Web site and hook it into the Web.config of the root site. So I added my assembly and the SharpZip assembly into my root site’s BIN directory. Fired up my local root Web site and tested the zip file creation – all’s working on that end.
But… and there’s always a but – adding this module to the root site fucks up every single one of my sub sites! So my WebLog and West Wind Web Store which are virtual directories defined below the Web Root (localhost/weblog, localhost/webstore etc.). Each is in a separate path off the root.
The problem is that the Web.config is inherited from the root web down into the virtual applications and I get the following error in my sub-application (the WebLog in this case)
Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.
Parser Error Message: Could not load file or assembly 'WestwindWebSiteComponents' or one of its dependencies. The system cannot find the file specified. (C:\westwind\web.config line 40)
Source Error:
Line 38: -->
Line 39: <httpModules>
Line 40: <add type="WestwindWebSiteComponents.ExeZipModule,WestwindWebSiteComponents" name="ExeZipModule" />
Line 41: </httpModules>
Line 42: </system.web> |
Source File: C:\westwind\web.config Line: 40
The error is telling me that the module doesn’t exist in the Weblog application. So the sub applications are inheriting the root application’s web.config always.
So far, I’ve been able to get away with running without a Web.config altogether so this has never come up. What sucks about this this handler/module loading is that the root web.config is used, but it’s not smart enough to actually use the root directory for finding the module and instead trying to look for it in the lower level application…
There are a workarounds, but none of them are pretty. I can GAC my component and SharpZipComponent and in that case each of the sub applications automatically loads this module. I don’t really consider this a great option, because the sub applications shouldn’t have to load this module!
I can go into each of the Web.config files of the sub applications and add a remove tag to the httpModules section. This is a bit painful, because there are around 25 apps running on this server and updating each one of these is not really an option.
In the end the solution I used was to use Global.asax and HttpApplication.Init() to hook the handler in code:
public WestwindWebSiteComponents.ExeZipModule ExeZip;
void Application_Start(object sender, EventArgs e)
{
}
public override void Init()
{
base.Init();
ExeZip = new WestwindWebSiteComponents.ExeZipModule();
ExeZip.Init(this);
}
…
And now I got a working module that fires only on the root Web site.
It’s an easy and relatively clean solution once you get over the surprise of the inheritance structure of the site.
I still think that the hierarchical nature of the Web Root doesn’t do what I would expect. When I think of the Web Root I think of it as just another virtual directory/application. To have the settings of the root affect all sub virtuals can cause all sorts of issues.
Moral of the story for me? Keep the web.config of the root clean.
FWIW if anybody’s interested here’s the code for the Exe->Zip Module.
/// <summary>
/// Module that pops automatically creates .Zip files from
/// Exe files and stores them in the local file system. Checks
/// for date stamps and creates new zip files only if the EXE
/// file is newer than the
/// </summary>
public class ExeZipModule : IHttpModule
{
object SyncLock = new object();
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
public void context_BeginRequest(object sender, EventArgs e)
{
System.Web.HttpApplication App = (System.Web.HttpApplication)sender;
string Path = App.Request.Path.ToLower();
if ( !Path.EndsWith(".exe.zip") )
return;
string ZipFilePath = App.Server.MapPath(Path);
string FilePath = ZipFilePath.Replace(".zip", "");
FileInfo fiFile = new FileInfo(FilePath);
FileInfo fiZip = new FileInfo(ZipFilePath);
bool FileExists = File.Exists(FilePath);
bool ZipExists = File.Exists(ZipFilePath);
if (!FileExists)
{
App.Response.StatusCode =400;
return ;
}
// If the file already exists we need to
// check if the exe is newer
if (ZipExists && FileExists )
{
if (fiFile.CreationTimeUtc <= fiZip.CreationTimeUtc)
{
// File already exists and is not newer - just return to client
App.Response.TransmitFile(ZipFilePath);
App.Response.End();
return;
}
}
// *** Protect section for dupe access
lock (SyncLock)
{
// *** Double check file creation
if (!File.Exists(ZipFilePath))
{
try
{
using (ZipFile zf = ZipFile.Create(ZipFilePath))
{
zf.BeginUpdate();
zf.Add(FilePath);
zf.CommitUpdate();
zf.Close();
}
}
catch (Exception ex)
{
if (File.Exists(ZipFilePath))
File.Delete(ZipFilePath);
App.Response.StatusCode = 404;
App.Response.End();
return;
}
}
App.Response.TransmitFile(ZipFilePath);
App.Response.End();
}
}
}
Note if you plan to use this (or something similar) you’ll need to map the .zip extension to the ASP.NET ISAPI Dll and make sure that the NETWORK SERVICE or ASPNET account has rights to write files into the download directory.
Other Posts you might also like