Saturday, January 17, 2015

Using IIS Express with remote systems

Traditionally, trying to develop and debug web-based projects through Visual Studio had been a pain. Generally, you had two less-than-awesome options:

  • Use the built-in Visual Studio web server, which lacked a number of features that a production IIS server would have
  • Use a local instance of IIS, which required your to run Visual Studio as an administrator.

To fix this problem, sometime around Visual Studio 2010, Microsoft released a local, single-site, user-mode version of IIS called IIS Express. While IIS Express can technically be used completely separate from Visual Studio, there is now a very tight integration between the two utilities, and the vast majority of use that IIS Express sees is as the new default Visual Studio web server. Since this process is launched by Visual Studio, and runs in the same user context, it's easy to attach to it and step into code running under the server. In fact, Visual Studio basically does all the work for you, so to a developer the process appears seamless.

There is one down side, however: out of the box, it only works if you bind to localhost. The reason for this is actually simple: IIS Express, like big-brother IIS, relies on the Windows service called HTTPD.SYS to set up HTTP protocol bindings on various ports, and HTTPD.SYS (by default) has very strict security: application running as a normal user aren't allowed to bind to any low-numbered ports, and aren't allowed to bind to anything besides localhost. On top of that, IIS Express installs its own self-signed SSL certificate and associates it with the typical range of SSL ports used by VS projects (port 443, which IIS proper owns, is already off-limits.)

In many cases that works fine, but it means there are some things you cannot easily do:
  • Test your application using remote clients.
  • Test multiple connections at one time
  • Easily test "real" SSL connections (such as the MVC RequireHttps attribute)
Fortunately, this isn't that hard to fix. You just need to let Windows know that you're OK with letting IIS Express bind to an external, and giving it a real certificate to bind to.

Step One: IIS Express Setup


The first thing you need to do is reconfigure the IIS Express site bindings for your project. When you create a new web-based project in Visual Studio and tell it to use IIS Express, it will ask if you want to create a new virtual directory. What really happens here is that Visual Studio is adding a tag to the list of in the IIS Express configuration.

The IIS Express configuration is done directly through the XML configuration file, which is found at:

C:\Users\[username]\Documents\IISExpress\config\applicationhost.config

If your project is already set up to work with IIS Express, you'll find a configuration block starting around 150 lines into the file -- look for the XML tag, and you'll find a element:

<site name="MySolution.MyProject" id="2">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/"          
                          physicalPath="C:\Projects\MySolution\MyProject" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:50000:localhost" />
    </bindings>
</site>

Inside that element is the list of ports and hostnames that IIS Express binds to when running that particular site. To set up a "proper" site, including full SSL, you just need to add a new binding element:

    <bindings>
        <binding protocol="http" bindingInformation="*:50000:localhost" />
        <binding protocol="http" bindingInformation="*:44300:localhost" />
        <binding protocol="http" bindingInformation="*:50000:desktop.kutulu.org" />
        <binding protocol="http" bindingInformation="*:44300:desktop.kutulu.org" />
    </bindings>

One thing to be careful of: you should never try to tell Visual Studio to use any of these other bindings as the "Site URL". Always let Visual Studio think the site is hosted at the non-SSL, localhost URL. If you try to reconfigure Visual Studio to use one of those other bindings, more often than not you will end up confusing Visual Studio, which will continually try to recreate the <site> and produce dozens of duplicate site configurations, none of which are what you want.

You should, however, tell Visual Studio that SSL is enabled, and what port it's one. For some reason, those settings are very well hidden: you have to select your project node in Solution Explorer and use the "F4" Properties Window -- not the project properties dialog -- to enable SSL and specify the port.

Step Two: Fix WCF


NOTE: This step is only needed for WCF services hosted by IIS Express. Other web-based projects should already behave this way.

By default, WCF services only use to a single binding from their hosting process. For your typical IIS-hosted WCF service, that's fine, because the binding will be port 80 on all IP addresses. For IIS Express, that's not going to fly: we have as many as four bindings we want to use. So, we need to tell the WCF engine to do some more work.

In your web.config file, find the <system.serviceModel> element, and make sure the following elements are in there somewhere:

<protocolMapping>
      <add scheme="https" binding="wsHttpBinding" bindingConfiguration="" />
    </protocolMapping>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true">

Step Three: Fix HTTPD.SYS


Full disclosure: this step is optional if you are willing to run Visual Studio and IIS Express as an admin user. But that defeats the entire purpose of IIS Express, which is a user-mode web server, so don't do that.

Instead, you just need to use the netsh command to reconfigure HTTPD.SYS to allow you to bind to the ports you want. Specifically, you need to use the http add urlacl command. Launch an administrative command prompt and/or PowerShell prompt and do this:

netsh http add urlacl url=http://desktop.kutulu.org:50000 user=Everyone

This will allow any user-mode process to create an HTTP binding on port 50000 using the real hostname of the machine, instead of localhost. Of course, repeat this process for any other ports you want to use for IIS Express. If you want to use SSL, you'll also need to do this for the SSL ports (traditionally starting with port 44300 and going up.)

Step Four: Fix HTTPD.SYS, SSL Version


Using IIS Express with SSL has two problems: binding your public IP address, and getting a usable certificate in place. When IIS Express is installed, it generates a self-signed certificate and associates that with all SSL connections that it hosts. That's rarely going to work for remote clients, and isn't really a valid test even for local clients. So, we need to tell HTTPD.SYS to use a different certificate.

I'm not going to go over the process of making a real certificate here. There are multiple ways to do this, my preference is to use my personal OpenSSL CA (who's root certificate I publish to anyone who needs it), but you can also use a public CA, or your corporate CA, or whatever works for you. The key points is, once you get the SSL certificate installed on your development machine, you need to get it's thumbprint. This is found by checking out it's properties in the Certificate Manager, and is just a sequence of hexadecimal digits (sometimes identified as the SHA Hash.) Or, if you're a PowerShell kinda person, you can do this:

Get-ChildItem Cert:\LocalMachine\My | 
 ? { $_.Issuer.Contains("kutulu.org") } | 
 % { $_.Thumbprint + ": " + $_.Subject + " (Expires " + $_.NotAfter + ")" }

Once you have the thumbprint, you need to associate it with the SSL ports you want to use. To do that, we use another netsh command, the http add sslcert command, which looks like this:

netsh http delete sslcert ipport="0.0.0.0:44300"
netsh http add sslcert ipport="0.0.0.0:44300" certhash="1234567890abcdef1234567890abcdef12345678" appid='{214124cd-d05b-4309-9af9-9caa44b2b74a}'

Some things to note here:
  • We first delete any existing binding; there may not be one, but if there is, you can't overwrite it, you have to delete and re-add it.
  • We are using IP address 0.0.0.0, which means the certificate will be associated with any IP address we bind to. You can have different certificates for different ports, if needed.
  • The appid given above is the one for IIS Express; since that's the app we plan to host our sites, we may as well use it's app ID.

Postscript: Powershell To The Rescue


I wrote myself a small Powershell script to go through and do this for a whole range of ports:

$IISHost = "desktop.kutulu.org"
$CertHash = "abcdef0123456789abcdef0123456789abcdef01"

$LowPort = 50000
$LowSSLPort = 44300
$RangeSize = 99

for ( $i = 0; $i -le $RangeSize; $i++ ) 
{
  netsh http delete urlacl url="http://${IISHost}:$($LowPort + $i)/"
  netsh http add urlacl url="http://${IISHost}:$($LowPort + $i)/" user=Everyone
  netsh http delete urlacl url="https://${IISHost}:$($LowSSLPort + $i)/"
  netsh http add urlacl url="https://${IISHost}:$($LowSSLPort + $i)/" user=Everyone

  netsh http delete sslcert ipport="0.0.0.0:$($LowSSLPort + $i)"
  netsh http add sslcert ipport="0.0.0.0:$($LowSSLPort + $i)" certhash="$CertHash" appid='{214124cd-d05b-4309-9af9-9caa44b2b74a}'
} 

Epilogue


Once you have done all of this, you will need to shut down IIS Express and let Visual Studio restart it. Once that's done, if you check out the IIS Express status icon in the notification area, you can see the entire list of site bindings for each running site. If that worked, you can now share your site with any other developers, QA testers, or curious management types that want to see it.