muonlab » 2010 » September

random .NET and web development musings

If you do:

Response.StatusCode = 503;

from an ASP.NET request, you will most likely get a response that looks like this:

HTTP/1.1 503 Service Unavailable
Cache-Control: private
Content-Type: text/html
X-Powered-By: ASP.NET
Date: Mon, 27 Sep 2010 17:23:20 GMT
Content-Length: 27

The service is unavailable.

I.E. “The service is unavailable” is the only content returned, regardless of what your response contained. This is because http.sys in IIS is overriding your response with default custom error behaviour. Great. Fix it like this:

Response.StatusCode = 503;
Response.TrySkipIisCustomErrors = true;

You can then return whatever content you like :D

The objectives for this post are to outline how to:

  • Create a user account in AD
  • Create a folder to hold the website code and assign the user read/execute rights
  • Grant the user access to use ASP.NET
  • Create an app pool running as our new identity
  • Create an iis site assigned to the app pool and pointing at our folder

First off you will need to add a reference to Microsoft.web.Administration.dll, which is in

c:\windows\system32\InetSrv

These are the namespaces you’ll need:

using System;
using System.Diagnostics;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Security.AccessControl;
using Microsoft.Web.Administration;

To begin we need to get a handle on AD, specifically the container where we want our user to be created:

var usersContext = new PrincipalContext(ContextType.Domain, "MyDomain", "ou=Users,ou=MyDomain,dc=MyDomain");

Next we create the user:

var webUser = new UserPrincipal(usersContext, "username", "password", true);
webUser.Save();

We then need to grant access to ASP.NET for the user (perhaps with v2 instead of v4)

Process.Start(@"c:\windows\microsoft.net\framework64\v4.0.30319\aspnet_regiis.exe", "-ga username");

Next we create the directory where we want the code to be housed, and grant the user read and execute permission on the folder:

var deploymentDir = Directory.CreateDirectory(fullPath);
var deploymentDirSecurity = deploymentDir.GetAccessControl();

deploymentDirSecurity.AddAccessRule(new FileSystemAccessRule("MyDomain\" + webUser.Name, FileSystemRights.ReadAndExecute, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));

deploymentDir.SetAccessControl(deploymentDirSecurity);

Now we can create the app pool:

var manager = new ServerManager();

var applicationPool = manager.ApplicationPools.Add("My new app pool");

applicationPool.ManagedPipelineMode = ManagedPipelineMode.Integrated;
applicationPool.ProcessModel.IdentityType = ProcessModelIdentityType.SpecificUser;
applicationPool.ProcessModel.UserName = parameters.Username;
applicationPool.ProcessModel.Password = parameters.Password;

And then the site:

var site = manager.Sites.Add("My new IIS site", "http", ":80:mydomain.com", fullPath);
site.Applications[0].ApplicationPoolName = "My new app pool";

manager.CommitChanges();

And there you have it :)

I’ve already got a bunch of lovely build scripts and remote management scripts that I won’t go into here, what I am going to talk about is how I’ve wired them together using MSDeploy to push my code to my remote server.

First off, install the Web Deployment Tool on your remote machine, but to help prevent haxing don’t put the remote agent service on the default URI. Instead I used a custom port and set up a firewall rule to only allow this port from our office’s IP address. Download the installer and run:

    msiexec /I WebDeploy_x64_en-US.msi /passive ADDLOCAL=ALL LISTENURL=http://+:1234/MSDeploy/

N.B. you can change the port and path to whatever you like.

Here are the installation docs.

Next, goto Services and start “Web Deployment Agent Service”, or run:

    net start msdepsvc

You probably also want to set it to Automatic instead of Manual. Then, set up your MSDeploy script to push your code:

    c:\program files\iis\microsoft web deploy\msdeploy.exe -verb:sync 
        -source:dirPath='d:\some\local\path'         
        -dest:dirPath='f:\some\remote\path',computerName=http://xxx.xxx.xxx.xxx:1234/MSDeploy,
            userName=remoteUser,password=remotePassword -verbose

N.B. “dirPath” is for pushing a whole directory, you can’t use this for a single file (I’m sure there’s a different switch for that though, see the docs, good luck.)

You can then execute a remote command:

    c:\program files\iis\microsoft web deploy\msdeploy.exe -verb:sync 
        -source:runCommand='f:\some\remote\path\somefile.bat',waitInterval=15000,waitAttempts=1
        -dest:auto',computerName=http://xxx.xxx.xxx.xxx:1234/MSDeploy,
            userName=remoteUser,password=remotePassword -verbose

The docs say that “waitAttempts” is the number of subsequent retry attempts so initially set it to 0, but I found that any waitInterval causes a timeout unless I set “waitAttempts” to 1 (presumably anything >0). Crap, but it works.

Note that in this example runCommand will call:

    "C:\Windows\System32\cmd.exe" /c "f:\some\remote\path\somefile.bat"

which means the current directory for the execution of the batch file is “C:\Windows\System32″. If you put:

    cd /d %~dp0

in your batch file, it will change the working dir to the path where the batch file resides. 1337.

Here are some docs.

MSDemply runCommand docs
MSDeploy Command line docs
MSDeploy Command line docs part deux (the bit you actually need)

Here is a simple jQuery plugin to allow users to filter table rows through dropdowns in the header rows.

Read more…