Skipping IIS Custom Error pages

By default, IIS7 intercepts 4xx and 5xx status responses with its own custom error pages. At work, we have a custom redirection module that checks if the status is 401 Unauthorized and spits javascript to redirect to the log in page. We use javascript in order to preserve # fragment in the return url.

The issue was 401. We set 401 to the response to send a meaninful response. The body contains a javascript redirection chunk. But it is intercepted by IIS7, so the user is not redirected but only see an dumb IIS 401 error page.

dumb_iis7_errror_page

 

 

After some googling, I found two ways to handle the issue. One is to let all response ignore IIS custom error pages. You can do that by setting existingResponse=”PassThrough” in the web.config.

<configuration>
  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
  </system.webServer>
</configuration>

 

The other is to set response.TrySkipIisCustomErrors = true, and then the only that response will be passed through without being intercepted by IIS7 custom error pages.

The second option was appropriate, as we want to pass through only for redirection module.

public void OnEndRequest(HttpContextBase context)
{
    if (context.Response.StatusCode != 401)
        return;

    var response = context.Response;
    response.TrySkipIisCustomErrors = true;
    response.Status = response.Status;
    response.StatusCode = (int)HttpStatusCode.Unauthorized;
    response.TrySkipIisCustomErrors = true;
    response.ClearContent();
    response.RedirectLocation = null;
    response.Write(_buildClientRedirectionResponse.GetRedirectionScript());
    response.End();
}

For me, TrySkipIisCustomErros = true didn’t work until you set a value to response.StatusCode. It seems that response.TrySkipIisCustomErrors = true and response.Status = response.Status should be set together.

With the second option, you can benefit from Custom error pages and a temporal pass through for 401 for redirection. Hope this helps.

Skipping IIS Custom Error pages

Recycling IIS application pools and COM+ programmatically in C#

These days, I work on projects that is based on Sitecore CMS. Sitecore caches everything, so if you make any changes to your code, the web page does not reflect your change until you reset IIS or recycle the application pool. Resetting IIS often takes 10 to 20, even 30 seconds, and it is a bit obstructive that you have to reset IIS in the middle of coding. So I wrote this small utility.

The way it works is straightforward. It accesses IIS application pools and recycle them all then access COM+ and recycle the specific application that my web application uses for database access. One tricky thing is to access COM+. You have to reference ComAdmin.dll in C:\windows\system32\Com and use COM interop. I could not find many examples on the Internet but finally found one at http://www.dotnet247.com/247reference/msgs/13/67515.aspx.

These are the references I found through google search.

This is my code.

class Program
{
    static void Main(string[] args)
    {
        ResetIIS();
        RecycleComPlus();
    }

    private static void ResetIIS()
    {
       DirectoryEntry appPools = new DirectoryEntry("IIS://localhost/W3SVC/AppPools");
       Console.WriteLine("Recycling the following application pools...\n");

       foreach (DirectoryEntry pool in appPools.Children)
       {
       try // if some app pools are turned off, it breaks.
       {
           pool.Invoke("Recycle", null);
           Console.WriteLine(pool.Name);
       }
       catch (Exception ex) {}
    }

    Console.WriteLine("\nCompleted...\n");
}

    private static void RecycleComPlus()
    {
        COMAdminCatalog catalog = new COMAdminCatalog();
        COMAdminCatalogCollection applications = (COMAdminCatalogCollection)catalog.GetCollection("Applications");
        applications.Populate();

        foreach (COMAdminCatalogObject app in applications)
        {
            if (string.Compare(app.Name.ToString(), "DataAccess") == 0)
            {
                Console.Write("Shutting down " + app.Name + "..");
                catalog.ShutdownApplication(app.Name.ToString());
                Console.WriteLine(". completed.");
            }
        }
    }
}

Hope this helps and leave comment if you have any idea to improve it.

Cheers

Recycling IIS application pools and COM+ programmatically in C#

Creating an IIS website change deployment script

Often, I need to deploy an website change to a production server. It can be a simple change like creating a virtual directory and rather rarely very big like setting up a new site. We export iis website setting, delete everything but the change, and import the change using vbscript.  The command is like this.

Set IIsComputer = GetObject("winmgmts://localhost/root/MicrosoftIISv2:IIsComputer='LM'")
...
IIsComputer.Import "", strFilePath, strSourceMetabasePath, strDestinationMetabasePath, intFlags

The xml file only contains the part that changed and has a basic skeleton tags

<?xml version="1.0" ?>
<configuration xmlns="urn:microsoft-catalog:XML_Metabase_V64_0">
<MBProperty>
<IIS_Global	Location ="."
		SessionKey="......."
	>
</IIS_Global>
<IIsWebServer	Location ="/LM/W3SVC/00"></IIsWebServer>
<IIsWebDirectory	Location ="/LM/W3SVC/00/ROOT/jsk"
   ...
	>
</IIsWebDirectory>
</MBProperty>
</configuration>
Creating an IIS website change deployment script

Visual Studio Add-in: Recycle all IIS application pools on build done

This is my 2nd visual studio-in. What it does is to recycle all application pools in IIS to make sure GAC is refreshed with newly built dlls.

There were a few things to tweak. This is an add-in for visual studio 2003, so it may not be suitable for vs 2005 or 2008 in some details.

  1. connectMode == ext_ConnectMode.ext_cm_Startup
    When the add-in wizard creates the skeleton code, ext_ConnectMode.ext_cm_UISetup is used. UISetup is only once when you set up the add-in, so the add-in is not loaded when you run this in debug-mode. Change it to …_Startup.
  2. _application.Solution.Projects
    In order to get projects I first tried ActiveSolutionProjects which worked perfectly fine in vs macro, but it did not work at all in add-in. ActiveSolutionProjects returned null if you build a solution. It returned only one project if you build a specific project. I used Solution.Projects and it worked as it is expected.

This is the code for recycling application pools.

private void RecycleAllApplicationPools()
{
DirectoryEntry appPools = new DirectoryEntry("IIS://localhost/W3SVC/AppPools");
_outputWindowPane.OutputString("-------------------------------------------\n");
_outputWindowPane.OutputString("Start recycling application pools\n");
foreach (DirectoryEntry appPool in appPools.Children)
{
appPool.Invoke("Recycle", null);
_outputWindowPane.OutputString(".");
}
_outputWindowPane.OutputString("\nRecycling is complete.\n");
}

The following is the full code

using System.DirectoryServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace ExtVS2003
{
using System;
using Microsoft.Office.Core;
using Extensibility;
using System.Runtime.InteropServices;
using EnvDTE;

#region Read me for Add-in installation and setup information.
// When run, the Add-in wizard prepared the registry for the Add-in.
// At a later time, if the Add-in becomes unavailable for reasons such as:
//   1) You moved this project to a computer other than which is was originally created on.
//   2) You chose 'Yes' when presented with a message asking if you wish to remove the Add-in.
//   3) Registry corruption.
// you will need to re-register the Add-in by building the MyAddin21Setup project
// by right clicking the project in the Solution Explorer, then choosing install.
#endregion

/// <summary>
///   The object for implementing an Add-in.
/// </summary>
/// <seealso class='IDTExtensibility2' />
[GuidAttribute("594C22F4-C9ED-40B1-9CC7-2D095D68AD97"), ProgId("ExtVS2003.Connect")]
public class Connect : Object, IDTExtensibility2, IDTCommandTarget
{
const string OUTPUTWINDOWGUID = "{1BD8A850-02D1-11D1-BEE7-00A0C913D1F8}";
private _DTE _application;
private AddIn addInInstance;
private Command _command;
private OutputWindowPane _outputWindowPane;
private BuildEvents _buildEvents;

/// <summary>
///        Implements the constructor for the Add-in object.
///        Place your initialization code within this method.
/// </summary>
public Connect()
{
}

/// <summary>
///      Implements the OnConnection method of the IDTExtensibility2 interface.
///      Receives notification that the Add-in is being loaded.
/// </summary>
///
<param term='application'>
///      Root object of the host application.
/// </param>
///
<param term='connectMode'>
///      Describes how the Add-in is being loaded.
/// </param>
///
<param term='addInInst'>
///      Object representing this Add-in.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
int bitmapNo = 59;
_application = (_DTE)application;
addInInstance = (AddIn)addInInst;

SetBuildEvent();
SetOutputWindowPane();

if (connectMode == ext_ConnectMode.ext_cm_Startup)
{
object []contextGUIDS = new object[] { };
Commands commands = _application.Commands;
_CommandBars commandBars = _application.CommandBars;

// When run, the Add-in wizard prepared the registry for the Add-in.
// At a later time, the Add-in or its commands may become unavailable for reasons such as:
//   1) You moved this project to a computer other than which is was originally created on.
//   2) You chose 'Yes' when presented with a message asking if you wish to remove the Add-in.
//   3) You add new commands or modify commands already defined.
// You will need to re-register the Add-in by building the ExtVS2003Setup project,
// right-clicking the project in the Solution Explorer, and then choosing install.
// Alternatively, you could execute the ReCreateCommands.reg file the Add-in Wizard generated in
// the project directory, or run 'devenv /setup' from a command prompt.
try
{
_command = commands.AddNamedCommand(addInInstance, "ExtVS2003", "Recycle IIS App pools", "Reset IIS", true, bitmapNo,
ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled);
CommandBar commandBar = commandBars["Tools"];
CommandBarControl commandBarControl = _command.AddControl(commandBar, 1);
}
catch(Exception ex)
{
MessageBox.Show("Cant't place toolbutton, error: " + ex.Message, "error", MessageBoxButtons.OK);
}
}

}

private void SetOutputWindowPane()
{
OutputWindow outputWindow = (OutputWindow)_application.Windows.Item(Constants.vsWindowKindOutput).Object;
_outputWindowPane = outputWindow.OutputWindowPanes.Item(OUTPUTWINDOWGUID);
}

private void SetBuildEvent()
{
_buildEvents = _application.Events.BuildEvents;
_buildEvents.OnBuildDone += new _dispBuildEvents_OnBuildDoneEventHandler(_buildEvents_OnBuildDone);
}

/// <summary>
///     Implements the OnDisconnection method of the IDTExtensibility2 interface.
///     Receives notification that the Add-in is being unloaded.
/// </summary>
///
<param term='disconnectMode'>
///      Describes how the Add-in is being unloaded.
/// </param>
///
<param term='custom'>
///      Array of parameters that are host application specific.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
{
try
{
_command.Delete();
}
catch (Exception ex)
{
MessageBox.Show("Error in Disconnect: " + ex.Message, "Error", MessageBoxButtons.OK);
}

}

/// <summary>
///      Implements the OnAddInsUpdate method of the IDTExtensibility2 interface.
///      Receives notification that the collection of Add-ins has changed.
/// </summary>
///
<param term='custom'>
///      Array of parameters that are host application specific.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnAddInsUpdate(ref Array custom)
{
}

/// <summary>
///      Implements the OnStartupComplete method of the IDTExtensibility2 interface.
///      Receives notification that the host application has completed loading.
/// </summary>
///
<param term='custom'>
///      Array of parameters that are host application specific.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnStartupComplete(ref Array custom)
{
}

/// <summary>
///      Implements the OnBeginShutdown method of the IDTExtensibility2 interface.
///      Receives notification that the host application is being unloaded.
/// </summary>
///
<param term='custom'>
///      Array of parameters that are host application specific.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnBeginShutdown(ref Array custom)
{
}

/// <summary>
///      Implements the QueryStatus method of the IDTCommandTarget interface.
///      This is called when the command's availability is updated
/// </summary>
///
<param term='commandName'>
///        The name of the command to determine state for.
/// </param>
///
<param term='neededText'>
///        Text that is needed for the command.
/// </param>
///
<param term='status'>
///        The state of the command in the user interface.
/// </param>
///
<param term='commandText'>
///        Text requested by the neededText parameter.
/// </param>
/// <seealso class='Exec' />
public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
{
if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
{
if(commandName == "ExtVS2003.Connect.ExtVS2003")
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|vsCommandStatus.vsCommandStatusEnabled;
}
}
}

/// <summary>
///      Implements the Exec method of the IDTCommandTarget interface.
///      This is called when the command is invoked.
/// </summary>
///
<param term='commandName'>
///        The name of the command to execute.
/// </param>
///
<param term='executeOption'>
///        Describes how the command should be run.
/// </param>
///
<param term='varIn'>
///        Parameters passed from the caller to the command handler.
/// </param>
///
<param term='varOut'>
///        Parameters passed from the command handler to the caller.
/// </param>
///
<param term='handled'>
///        Informs the caller if the command was handled or not.
/// </param>
/// <seealso class='Exec' />
public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if(commandName == "ExtVS2003.Connect.ExtVS2003")
{
RecycleAllApplicationPools();
handled = true;
return;
}
}
}

private void _buildEvents_OnBuildDone(vsBuildScope Scope, vsBuildAction Action)
{
if (IsBuildingGACProject())
{
RecycleAllApplicationPools();
}
}

private bool IsBuildingGACProject()
{
Regex GACRegex = new Regex("PJB.UI|PJB.Business", RegexOptions.IgnoreCase);
Projects projects = _application.Solution.Projects;

foreach (Project project in projects)
{
if (GACRegex.Match(project.Name).Success)
{
return true;
}
}
return false;
}

private void RecycleAllApplicationPools()
{
DirectoryEntry appPools = new DirectoryEntry("IIS://localhost/W3SVC/AppPools");
_outputWindowPane.OutputString("-------------------------------------------\n");
_outputWindowPane.OutputString("Start recycling application pools\n");
foreach (DirectoryEntry appPool in appPools.Children)
{
appPool.Invoke("Recycle", null);
_outputWindowPane.OutputString(".");
}
_outputWindowPane.OutputString("\nRecycling is complete.\n");
}
}
}

Visual Studio Add-in: Recycle all IIS application pools on build done

Install website in IIS using vbscript

Reference

In vbscript regular expression, you can use “SubMatches” collection to get the string you want to use. Before I found this, I had to get “Matches(0).Value” and replace unnecessary strings to empty string.

WScript.Echo FSO.FolderExists(“C:\temp\”)
Set Matches = regex.Execute(configLine)
WScript.Echo Matches(0).SubMatches(1)

The script parses website xml and does necessary actions. So, it uses regular expression heavily to parse the xml and get the right information. It has the following 5 operations

  • Get Site identifier from metabase xml file
  • Delete the site using identifier
  • Create all virtual directories if not exist
  • Install application pools and web site. This imports the xml.
  • Install SSL Certificate

This is the code

Dim fso, iisSiteId, webConfigPath
Set fso = CreateObject("Scripting.FileSystemObject")
webConfigPath = "..\IISSettings\Website.config"

iisSiteId = GetIisSiteIdentifier()      ' Read site identifier from xml file
If iisSiteId &gt; 0 Then
    DeleteWebsite iisSiteId             ' Delete the existing web site
End If

CreateVirtualDirectories(webConfigPath) ' Create virtual directories

Call InstallAppPoolsWebSite             ' Install application pools and web site
InstallSSLCertificate iisSiteId         ' Install certificate    

Sub CreateVirtualDirectories(path)
    'On Error Resume Next

    Dim fl, strm, strLine, regex, Matches, fldPath
    Set fl = FSO.GetFile(path)
    Set strm = fl.OpenAsTextStream(1, -2)
    Set regex = New RegExp
    regex.Pattern = "(^\s*Path="")(.+)(""\s*$)"

    WScript.Echo "Creating virtual directories ..."
    Do Until strm.AtEndOfStream
        strLine = strm.ReadLine
        If regex.Test(strLine) = True Then
            Set Matches = regex.Execute(strLine)
            fldPath = Matches(0).SubMatches(1)
            If fso.FolderExists(fldPath) = False Then
                WScript.Echo fldPath
                CreateFolderRecursive(fldPath)
            End If
        End If
    Loop
End Sub

Function CreateFolderRecursive(path)
    CreateFolderRecursive = False
    If Not fso.FolderExists(path) Then
        If CreateFolderRecursive(fso.GetParentFolderName(path)) Then
            CreateFolderRecursive = True
            Call fso.CreateFolder(path)
        End If
    Else
        CreateFolderRecursive = True
    End If
End Function

Sub InstallAppPoolsWebSite()
    'If DefaultAppPoolPresent = False Then
    'If DefaultAppPoolPresent("DefaultAppPool") = False Then
    '    Import fso.GetFile("..\IISSettings\DefaultAppPool.config")
    'End If
    WScript.Echo "Installing Application Pools and Website"

    Dim flder, fls, fl
    Set flder = fso.GetFolder("..\IISSettings\")
    Set fls = flder.Files
    For Each fl in fls
        If InStr(fl.name, ".config") &gt; 0 Then
            Import fso.GetFile(fl.Path)
        End If
    Next
End Sub

Sub InstallSSLCertificate (siteIdentifier)
    WScript.Echo "Installing SSL Certificate... to W3SVC/" &amp; iisSiteId

    Dim iisCertObj
    Set iisCertObj = CreateObject("IIS.CertObj")
    iisCertObj.InstanceName = "W3SVC/" &amp; siteIdentifier
    iisCertObj.Import "..\IISSettings\site_cert.pfx", "1234567", true, true
End Sub

''''''''''''''''''''''''''''''''''''''''''
' Find site identifier from settings xml '
''''''''''''''''''''''''''''''''''''''''''
Function GetIisSiteIdentifier()
    Dim SiteIdRegex, Matches
    Set SiteIdRegex = New RegExp
    SiteIdRegex.Pattern = "(""\/LM\/W3SVC\/)([0-9]+)("")"

    Dim fl, strm, strXml
    Set fl = fso.GetFile("..\IISSettings\Website.config")
    Set strm = fl.OpenAsTextStream(1, -2)
    Do
        strXml = strm.ReadLine
    Loop Until SiteIdRegex.Test(strXml) = True

    Set Matches = SiteIdRegex.Execute(strXml)
    GetIisSiteIdentifier = Matches(0).SubMatches(1)
End Function

''''''''''''''''''''''''''''''''
' Impoart App pool and website '
''''''''''''''''''''''''''''''''
Sub Import(objFile)
    Const IMPORT_EXPORT_MERGE     = 4
    Dim strPassword, IIsComputer, filePath, strXML
    Set IIsComputer = GetObject("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy}!//localhost/root/MicrosoftIISv2:IIsComputer='LM'")
    strPassword = ""
    filePath = objFile.Path

    Set stream = objFile.OpenAsTextStream(1, -2)

    Do
       strXML = stream.ReadLine
    Loop Until InStr(strXML, "&lt;&gt; 0 Or InStr(strXML, "&lt;&gt; 0

    intFirst = InStr(1, strXML, """")
    strSourceMetabasePath = Mid(strXML, intFirst + 1, InStr(intFirst + 1, strXML, """") - intFirst - 1)
    strDestinationMetabasePath = strSourceMetabasePath
    WScript.Echo "Meta: " &amp; strSourceMetabasePath
    intFlags = IMPORT_EXPORT_MERGE
    WScript.Echo "Importing: " &amp; filePath &amp; " " &amp; strSourceMetabasePath
    IIsComputer.Import strPassword, filePath, strSourceMetabasePath, strDestinationMetabasePath, intFlags

    Start strSourceMetabasePath
End Sub

Sub Start(strWebServer)
   On Error Resume Next
   Set IIsWebServer = GetObject("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy}!//localhost/root/MicrosoftIISv2:IIsWebServer='" &amp; Mid(strWebServer, 5) &amp; "'")
   IIsWebServer.Start
End Sub

''''''''''''''''''''''''''''''''''''''''''''''
' Check if DefaultApplicationPool is present '
''''''''''''''''''''''''''''''''''''''''''''''
'Function DefaultAppPoolPresent
Function DefaultAppPoolPresent(poolName)
    Dim appPools, appPool
    Set appPools = GetObject("IIS://localhost/W3SVC/AppPools")

    DefaultAppPoolPresent = False
    for each appPool in appPools
        if (appPool.Name = poolName) Then
            DefaultAppPoolPresent = True
        End If
    next
End Function

''''''''''''''''''''''''''''''''
' Delete the existing website  '
''''''''''''''''''''''''''''''''
Sub DeleteWebsite(identifier)
    On Error Resume Next
    Dim website
    Set website = Nothing
    Set website = GetObject("IIS://LocalHost/W3SVC/" &amp; identifier) 

    If website Is Nothing Then
        WScript.Echo "Cannot find the existing website: " &amp; identifier
    Else
        Set websiteParent = GetObject(website.Parent)
        WScript.Echo "Deleting: " &amp; website.Class &amp; " " &amp; website.Name
        websiteParent.Delete website.Class, website.Name
    End If
End Sub
Install website in IIS using vbscript