Friday 31 October 2014

Using IIS URL Rewrite

This article is about creating IIS URL Rewrite Rules using Powershell. The Powershell functions that we use apply generally to reading and writing to .NET configuration file (web.config, app.config, etc.), and so can be applied to tasks like updating connection strings.
The functions used are:
Get-WebConfigurationProperty
Add-WebConfigurationProperty
Set-WebConfigurationProperty

Although these are powerful, being well documented is not their strongest feature! There are also a couple of simple pitfalls with the URL Rewrite Rules, which I’ll point out. Note that for the following to work, you’ll need to have the IIS URL Rewrite extension installed in IIS. You can install this using the Web Platform Installer, or using Chocolatey

Files for this article can be found here

The test website

So we can test our rewrite rules we’ll use a simple website structured as follows:
image

Each leaf folder has two HTML files, 1.html and 2.html and there’s a page of links at the root:
clip_image002

To help test the rewrite rules, the HTML files indicate where they are in the structure:
clip_image003

Introduction to rewrite rules

The rules can be maintained in the IIS Manager by double clicking URL Rewrite in features view:
clip_image004

There is a rich GUI that lets you maintain the rules and which does validation against your input:
clip_image005

The above rule redirects any URL section matching “1.html” to “2.html”. You’ll see this in the site’s config file:
clip_image006

The rules are stored in the config file. The config file that’s changed depends on whether you apply the rules at the level of the virtual directory, website or the server. If the rules apply at the server level they are stored in the main IIS config file here:
%windir%\system32\inetsrv\config\applicationHost.config
After this rule has been applied, recycle the app pool or reset IIS and then when accessing http://localhost/simple-rewrite/1.html you’ll be taken to http://localhost/simple-rewrite/2.html.

Using Powershell

Firstly, make sure you have the WebAdministration module loaded:
Import-Module WebAdministration
To create the above rule using Powershell, run the following script (see CreateRule1.ps1):
Add-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules' `-name "." `-value @{name='Rule 1'; patternSyntax='ECMAScript'; stopProcessing='True'}
Set-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1"]/match' `-name url `-value '1.html'
Set-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1"]/action' `-name . `-value @{ type="Redirect"; url='2.html'; redirectType="SeeOther" }

The pspath parameter is the website path in the format used by the WebAdministration module. The filter parameter is the XPath to select the element we’re interested in. Here it’s under rules/rule and has a name attribute with the value “Rule 1”.

To remove the rule, run the following script (see RemoveRule1.ps1):
Clear-WebConfiguration `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1"]'

Note that Clear-WebConfiguration removes the rule using the name selector and will raise an error if the rule is not found. W If you want to test whether the rule exists, use Clear-WebConfiguration as follows:
$existing = Get-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1"]' -name *
if ($existing) {
Clear-WebConfiguration `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1"]'
}

Rule processing order and redirect exclusions

The rules are processed in the order they appear in IIS Manager and the config file. A rule can be set to stop processing of subsequent rules. These two combine to make an effective way to create an exception to a redirect. Say you wanted to redirect all “1.html” URLs to “2.html” except for “~b/c/1.html”. To achieve this add in the following rule above the more general redirect:
clip_image007

Your configuration will look something like:
clip_image008

Using rule conditions

Let’s say you want a rule that applies to POSTs to a particular URL but which only contain certain parameters in the query string. Such as a POST to:
~/b/2.html?paramA=Z

Let’s say we want to detect parameter values of X, Y or Z. You can do this using rule conditions. The completed rule looks like this in IIS Manager:
clip_image009

In the configuration, this rule looks like:
clip_image010

And can be scripted as follows:
Add-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules' `-name "." `-value @{name='Rule 1 Post Exception'; patternSyntax='ECMAScript'; stopProcessing='True'}
Set-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1 Post Exception"]/match' `-name url `-value "b/2.html"
Add-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1 Post Exception"]/conditions' `-name "." `-value @{input="{REQUEST_METHOD}"; pattern='POST'}
Add-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1 Post Exception"]/conditions' `-name "." `-value @{input="{QUERY_STRING}"; pattern='paramA=(X|Y|Z)'}


Notes and gotchas

Rules are relative to the site root

Once you realize that the redirect URLs are relative to the site root, it’s all very obvious. Gaining that realisation can take some time! So for the example site above, the following rule will do nothing:
clip_image011

IIS Manager has code completion for conditions

When you add a condition in IIS Manager, you get code completion after typing the first “{“ (such as in the POST exception rule above):
clip_image012

Files get locked and browsers cache pages

When you’re developing the rules config file is being touched by TIIS, IIS Manager, Notepad++, Powershell and goodness know what else. Your super-modern browser is being super-modern helpful and caching the HTML pages. Sometimes you just need to recycle the app pool to see the updated rule. Other times, you need to do an IIS reset, clear your browser cache and restart your tool windows.

Default values are not written

When the rule’s action type is None and it stops processing of other rules, the configuration written by the IIS Manager GUI is like this:
clip_image013

Following the same pattern for creating the rule in a script, you can run something like this (see CreateExceptionRule.ps1):
Add-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules' `-name "." `-value @{name='Rule 1 Exception'; patternSyntax='ECMAScript'; stopProcessing='True'}
Set-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1 Exception"]/match' `-name url `-value 'b/c/1.html'
Set-WebConfigurationProperty `-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `-filter '/system.webserver/rewrite/rules/rule[@name="Rule 1 Exception"]/action' `-name . `-value @{ type="None" }

The above script writes the following to the configuration file.
  image

Note that the action element is not written. It seems that the Powershell command knows that <action type=”None”> is the default and doesn’t bother to write it, but the IIS Manager GUI writes it anyway. If you didn’t know that you may spend a lot of time trying to work out why your Powershell isn’t doing what you think it should!












































14 comments:

Unknown said...

In this script how to supply the name of the rule through a variable

Sean Kearon said...

Hi Abhishek

If you are starting out with something like this:

Add-WebConfigurationProperty `
-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules' `
-name "." `
-value @{name='Rule 1'; patternSyntax='ECMAScript'; stopProcessing='True'}

Then you can use something like the following to set the name using a variable:

$ruleName = 'Rule 1'
Add-WebConfigurationProperty `
-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules' `
-name "." `
-value @{name=$ruleName; patternSyntax='ECMAScript'; stopProcessing='True'}

I've not tested that and you may need to use "$ruleName" instead of $ruleName in the last line. Let me know how you get one.

Sean

Unknown said...

Hello,

Add-WebConfigurationProperty takes the value

But for Set-WebConfigurationProperty
gives error whether we write in doublequotes or without quote

Sean Kearon said...

Hi Abhishek

PowerShell only expands variables in strings that are enclosed in double quotes. Changing the string to use double quotes and then using the backtick character to escape the enclosed quotation marks will do what you need. For example:


$ruleName = 'Rule 1'
Add-WebConfigurationProperty `
-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter '/system.webserver/rewrite/rules' `
-name "." `
-value @{name=$ruleName; patternSyntax='ECMAScript'; stopProcessing='True'}

Set-WebConfigurationProperty `
-pspath 'IIS:\Sites\Default Web Site\simple-rewrite' `
-filter "/system.webserver/rewrite/rules/rule[@name=`"$ruleName`"]/match" `
-name url `
-value '1.html'

Unknown said...

Hello Sean,

It worked Thanks !!

Hp Android dan Paket Internet said...

very perfect tutorial thanks a lot..

KP said...

Hi,

I want to add a rewrite rule at server / root level, how can I do that?

For the default website it is:
-pspath 'IIS:\Sites\Default Web Site\'

but what should the -pspath be to apply a rule globally?

Thanks!

Sean Kearon said...

Hi KP

Try using -pspath 'iis:.

Cheers

Sean

KP said...

Hi Sean,

Thanks for your suggestion. Unfortunately using -pspath 'iis:' still adds the rule to the default website.

When I check the applicationHost.config the rule is added to the rules and not to globalRules:

<rewrite>
<globalRules>
</globalRules>
<rules>
<rule name="Rule 1" stopProcessing="true" />
</rules>
</rewrite>

Any other suggestion?

Sean Kearon said...

Hi KP

When you add a rule at the server level, it will show up in all sites.

To check that, add another site and you should also see the rule in there.

It is worth noting that it is possible for a site to override/disable a rule that is applied at the server level. Very useful if you ever need that!

Cheers

Sean

KP said...

Hi Sean,

We now already have all our rewrite rules listed globally, so that would be a bit confusing.

I found out that this works for me:

Add-WebConfigurationProperty -PSPath "IIS:" -Filter "/system.webServer/rewrite/globalRules" -Name "." -Value @{ name = $name }

Thanks for your suggestion anyway,

Unknown said...

How can I update the rules order with Powershell ?

Sean Kearon said...

I think you would need to remove the sections you wanted to change using Remove-WebConfigurationProperty and then add then back in the order you wanted them.

Halcy0n said...

I had the same question about reordering and it looks like there is some basic facility to do that.

When creating the rule you can use -AtIndex 0, -AtIndex 1, etc.. Specifying the index does require you to have a concept of how many rules you'll be using since you cannot have only 1 rule in existence and then set the index to 2. But a handy trick with AtIndex 0 is that you can guarantee that if you are adding a rule later down the road that it can jump to being your first rule. This won't be applicable for everyone but I was looking for the same question and stumbled on here.

Example: Add-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST/Default Web Site' -filter "system.webServer/rewrite/rules" -AtIndex 1 -name "." -value @{name='YourRuleName';stopProcessing='True'}