As the administrator of multiple computers, I want the machines I run to be as stable and secure as possible. (Duh!)
One way I go about that is by not installing anything that isn't absolutely necessary.
So whenever I see a dependency that's providing only a tiny bit of functionality, I try to get rid of it.
Such was the case with the URL Rewrite module for IIS when I migrated my blog to Node.js.
Aside: I'm not hating: URL Rewrite does some cool stuff, lots of people love it, and it's recommended by Microsoft.
However, my sites don't use it, so it represented a new dependency and was therefore something I wanted to avoid. :)
The role URL Rewrite plays in the Node.js scenario is minimal - it points all requests to app.js
, passes along the original URL for the Node app to handle, and that's about it.
Tomasz Janczuk outlines the recommended configuration in his post Using URL rewriting with node.js applications hosted in IIS using iisnode.
I figured I could do the same thing with a few lines of code by defining a simple IHttpModule implementation in App_Code
.
So I did! :)
The class I wrote is named IisNodeUrlRewriter
and using it is easy.
Starting from a working Node.js application on iisnode (refer to Hosting node.js applications in IIS on Windows for guidance), all you need to do is:
- Put a copy of
IisNodeUrlRewriter.cs
in the App_Code
folder
- Update the site's
web.config
to load the new module
- Profit!
Here's what the relevant part of web.config
looks like:
<configuration>
<system.webServer>
<modules>
<add name="IisNodeUrlRewriter" type="IisNodeUrlRewriter"/>
</modules>
</system.webServer>
</configuration>
Once enabled and running, IisNodeUrlRewriter
rewrites all incoming requests to /app.js
except for:
- Requests at or under
/iisnode
, iisnode's default log directory
- Requests at or under
/app.js/debug
, iisnode's node-inspector entry-point
- Requests at or under
/app.js.debug
, the directory iisnode creates to support node-inspector
If you prefer the name server.js
, you want to block one of the above paths in production, or you're running Node as part of a larger ASP.NET site (as I do), tweak the code to fit your scenario.
I modified the standard Node.js "hello world" sample to prove everything works and show the original request URL getting passed through:
require("http").createServer(function (req, res) {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Requested URL: " + req.url);
}).listen(process.env.PORT, "127.0.0.1");
console.log("Server running...");
Sample output:
http://localhost/app.js
Requested URL: /app.js
http://localhost/
Requested URL: /
http://localhost/any/path?query=params
Requested URL: /any/path?query=params
I also verified that IIS's default logging of requests remains correct (though you can't tell that from the output above).
Note: I've found that accessing the debug path through IisNodeUrlRewriter
can be somewhat finicky and take a few tries to load successfully.
I'm not sure why, but because I don't use that functionality, I haven't spent much time investigating.
The implementation of IisNodeUrlRewriter
is straightforward and boils down to two calls to HttpContext.RewritePath that leverage ASP.NET's default execution pipeline.
One handy thing is the use of HttpContext.Items to save/restore the original URI.
One obscure thing is the use of a single regular expression to match all three (actually six!) paths above.
(If you're not comfortable with regular expressions or prefer to be more explicit, the relevant check can easily be turned into a set of string comparisons.)
The implementation is included below in its entirety; it ends up being more comments than code!
I've also created a GitHub Gist for IisNodeUrlRewriter in case anyone wants to iterate on it or make improvements.
(Some ideas: auto-detect the logging/debug paths by parsing web.config
/iisnode.yml
, use server.js
instead of app.js
when present, support redirects for only parts of the site, etc.)
using System;
using System.Text.RegularExpressions;
using System.Web;
public class IisNodeUrlRewriter : IHttpModule
{
private const string ItemsKey_PathAndQuery = "__IisNodeUrlRewriter_PathAndQuery__";
private Regex _noRewritePaths = new Regex(@"^/(iisnode|app\.js[/\.]debug)(/.*)?$", RegexOptions.IgnoreCase);
public void Init(HttpApplication context)
{
context.BeginRequest += HandleBeginRequest;
context.PreRequestHandlerExecute += HandlePreRequestHandlerExecute;
}
private void HandleBeginRequest(object sender, EventArgs e)
{
var application = (HttpApplication)sender;
var context = application.Context;
if (!_noRewritePaths.IsMatch(context.Request.Path))
{
context.Items[ItemsKey_PathAndQuery] = context.Request.Url.PathAndQuery;
context.RewritePath("/app.js");
}
}
private void HandlePreRequestHandlerExecute(object sender, EventArgs e)
{
var application = (HttpApplication)sender;
var context = application.Context;
var originalPath = context.Items[ItemsKey_PathAndQuery] as string;
if (null != originalPath)
{
context.RewritePath(originalPath);
}
}
public void Dispose()
{
}
}