This post is about how to run your favorite rack application on IIS 7 using IronRuby. I’ve been unsatisfied with most other windows ruby app hosting I’ve tried, and IronRuby-Rack looks like it will fix that. (I haven’t tried deploying to JRuby on Windows, but I assume that experience would be pretty good.)

Surely I’m not the first to the punch on this, but there were some things I had to figure out that I thought I’d share.

I’m doing this in the context of a sinatra application I’m writing. More on the specific app later, but it wasn’t worth writing if it wasn’t going to run on IIS, or at least on Windows.

Also, I tried the ironruby-rack gem, but it’s pretty rough at this point. The best thing about it is that it included IronRuby.Rack.dll. My major complaint is that it put web.config in the root of the app, which meant that all the .rb files are in the web root. It seemed much classier to make public the web root, with web.config in there.

It wasn’t too hard to get the app running.

A rackup file seemed like a sensible first step, and it was. You can’t get very far these days without a rackup file.

I snagged IronRuby.Rack.dll from the ironruby gem, and checked it in public/bin. This was done because I’m lazy and didn’t want to build it myself. It’d be really nice if IronRuby.Rack was a stand-alone github project so I could fork it and patch it. Cloning all of ironruby just for a version of IronRuby.Rack that probably isn’t current wasn’t very interesting to me.

My rake tasks build the rest of the aspnet application. The tasks are aspnet:copybin, aspnet:logdir, aspnet:webconfig, and aspnet. The last just invokes the others.

aspnet:copybin finds IronRuby.Rack’s dependencies in the current ironruby environment and copies them into public/bin.

aspnet:logdir creates a directory for IronRuby.Rack to put its logs into. IronRuby.Rack is fussy about this directory existing, and about its ability to write to said directory.

aspnet:webconfig is more interesting. The web.config file it generates sets up the ASP.NET handler for ironruby.rack and tells it where everything is. I do bindingRedirects so that IronRuby.Rack can find the IronRuby version that I grabbed in aspnet:copybin. I started with the templates in the ironruby-rack gem and trimmed it down to what my app needed.

Here’s what I learned while crafting the web.config file:

IronRuby.Rack includes two hooks for ASP.NET: a module and a handler. The module seemed like the way to go, so I tried it first. I was a bit disappointed that it grabbed each request at the beginning of the application pipeline, and called EndRequest. It would have been fine if I didn’t care about anything that IIS was doing for me, but I did. I needed other modules to run (particularly the WindowsAuthentication module), and having IronRuby short-circuit the process broke that. I switched to the handler, and was much happier.

Also, IronRuby.Rack doesn’t mess with Environment.CurrentDirectory at all, so if your app needs to know about the directory it lives in, you need to tell it about that. Rails is pretty tolerant about this, with its Rails.root stuff, but bundler isn’t. Bundler was looking in c:\windows for my Gemfile. My first impulse was to set environment variables in web.config, but IronRuby.Rack doesn’t have hooks for that. So my app.rb has another bit of bundler bootstrapping that most apps can leave out: ENV['BUNDLE_GEMFILE'] ||= File.expand_path(__FILE__ + '/../Gemfile')

As a nice side-effect of using ASP.NET, to restart the application I just need to “rake aspnet:webconfig”. ASP.NET reloads the application whenever web.config changes.

Github is where to go to see the complete Rakefile.

On my current project, I was tasked with adding a WebDAV folder to an ASP.NET application. I found a WebDAV component on sourceforge, and thought I’d give it a try, prior to investigating the less-free options. It took an hour or so to get the sample web application working. It looked like it had the hooks necessary in order to tie into the existing authentication and authorization in the application, so I started integrating it into the application.

My first pass at integrating it was to do everything at once: tie in WebDAV.net, add authentication and authorization, and change the WebDAV/file mapping from root = root to root/a/b/c = root. Needless to say, this was too big of a chunk to bite off.

So step 1 turned into getting it so that I could open the application with a WebDAV client. This involved tweaking another HttpModule that was throwing a NullReferenceException because app.User wasn’t defined. Things seemed to work, so I checked it in.

Step 2 was to tie in auth. I got the WebDAV component to set the response status to 401, but it and the Forms authentication disagreed about what that meant: WebDAV wanted to send a HTTP Basic Auth request, and ASP.NET wanted the user to be redirected to the login page. After fighting with this for a day or so, it became clear that the WebDAV part of the site would need to be a separate ASP.NET application. Also, another dev pointed out that the WebDAV module was messing up other parts of the site.

So I created a new project and plopped the WebDAV module in it. It took some time to get it back to the point where it was running (the HttpModule wasn’t being loaded at one point, and it wasn’t giving me either warnings or errors). Next was to create a connection to the database…

I tried copying over configuration from the main application to the new application in order to get all of the dependency injection stuff to work on the WebDAV application. This way I could do ServiceLocator<ILoginDataProvider>.Get(), just like the main application did when the user logged in. I was creeping towards this, when I realized that most of the high-level auth code in the main application assumed that it was running in a forms-based authentication environment (e.g. the data provider pulled information from the session, if it was constructed with the CompositeWeb services container), and I needed to do a protocol-based authentication.

I had an epiphany at this point — why complicate things by using CompositeWeb when all I needed was to make two calls to the LoginDataProvider? Two concrete constructor calls later, I had auth implemented for the WebDAV web application.

Along the way, I discovered that the best way to test was to set up a virtual directory in IIS. Visual Studio was helpful enough to create the virtual directory of my choosing. In order to get WebDAV to work correctly, I needed to add the ASP.NET ISAPI dll as a wildcard handler, and I needed to tell IIS that there would never be any authentication (i.e. disable digest, basic, and integrated authentication). Disabling integrated authentication makes VS tell you that you can’t debug the application, but this is a lie… Build the application, then Debug, Attach to Process, and pick aspnet_wp.exe. You just can’t use the F5 shortcut that normally does all of that for you.

So, here’s what I’d say about integrating WebDAV into an existing ASP.NET application:

  • WebDAV.net works fine (though the framework has a couple of very obvious bugs when you don’t set up debug logging).
  • Make it a separate application. If you want it to look like it’s a subfolder of your application, deploy it to a subdirectory of the application.
  • Don’t try to use CompositeWeb and WebDAV.net at the same time.