Using IIS configuration tools to manage HWC configuration

I have seen many people built some innovative solutions on top of hostable web core (HWC) functionality which was added in IIS7. One of the questions which HWC users frequently ask is how to make IIS configuration tools (like appcmd, UI etc.) work against configuration file which their HWC instance is using. So far they have been either doing manual modifications to HWC configuration file or making changes to IIS configuration and then porting over to HWC configuration. There is an easier way to do this but caveat is that this requires your HWC configuration file name to be applicationHost.config. If that is the case, you can use IIS shared configuration feature to setup a redirection to your HWC configuration file. Below are the steps to get this working.
 
          Open %windir%\system32\inetsrv\config\redirection.config.
          Change <configurationRedirection /> to <configurationRedirection enabled=”true” path=”c:\HWCInstance\” />
          Save and exit.
 
With this change, configuration system will map MACHINE/CONFIG/APPHOST to c:\HWCInstance\applicationHost.config and assume this as the main configuration file containing applicationPools and sites information. Now appcmd, UI etc. will work against HWC configuration file. Please note that setting up this redirection will affect your main instance of IIS as well. So if you are using IIS and HWC in parallel, this won’t be a safe thing to do. But if you are not using IIS main instance and only running your own HWC instance, this can be used to make IIS configuration tools work against your HWC configuration.
 
Hope this helps.
Kanwal

Implementing IAppHostPathMapper in C

Few days ago I was required to implement IAppHostPathMapper interface in native C to map configuration path MACHINE/WEBROOT/APPHOST to DefaultAppPool.config and struggled with finding good documentation. With help of some incomplete, hard to find documentation and some head banging here is what worked for me. Hopefully this will be useful for few others J.

#include <ahadmin.h>

//
// PathMapper object structure with VTable pointer and reference counter
// Add other private data in this structure
//
typedef struct TestPathMapper
{
    IAppHostPathMapperVtbl * lpVtbl;
    ULONG                    cRef;
} TestPathMapper;

//
// Method definitions
//
STDMETHODIMP TestPathMapper_QueryInterface(TestPathMapper *, REFIID, LPVOID FAR *);
STDMETHODIMP_(ULONG) TestPathMapper_AddRef(TestPathMapper *);
STDMETHODIMP_(ULONG) TestPathMapper_Release(TestPathMapper *);
STDMETHODIMP TestPathMapper_MapPath(TestPathMapper *, BSTR, BSTR, BSTR *);

//
// IAppHostPathMapper VTable structure
//
static const IAppHostPathMapperVtbl vtblTestPathMapper =
{
    TestPathMapper_QueryInterface,
    TestPathMapper_AddRef,
    TestPathMapper_Release,
    TestPathMapper_MapPath
};

STDMETHODIMP
TestPathMapper_QueryInterface(
    TestPathMapper * pThis,
    REFIID           riid,
    LPVOID FAR *     lppvObj
)
{
    if ( !pThis || pThis->lpVtbl != &vtblTestPathMapper || !lppvObj )
    {
        return E_INVALIDARG;
    }

    if ( !memcmp(riid, &IID_IUnknown, sizeof( IID ) ) ||
         !memcmp(riid, &IID_IAppHostPathMapper, sizeof( IID ) ) )
    {
        pThis->lpVtbl->AddRef( pThis );
        *lppvObj = pThis;

        return S_OK;
    }

    *lppvObj = NULL;
    return E_NOINTERFACE;
}

STDMETHODIMP_(ULONG)
TestPathMapper_AddRef(
    TestPathMapper * pThis
)
{
    if (!pThis || pThis->lpVtbl != &vtblTestPathMapper)
    {
        return 1;
    }

    return InterlockedIncrement( &pThis->cRef );
}

STDMETHODIMP_(ULONG)
TestPathMapper_Release(
    TestPathMapper * pThis
)
{
    LONG cRef;   
    if ( !pThis || pThis->lpVtbl != &vtblTestPathMapper )
    {
        return 1;
    }

    cRef = InterlockedDecrement( &pThis->cRef );
    if (cRef == 0)
    {
        pThis->lpVtbl->Release(pThis);
        pThis->lpVtbl = NULL;
        free( pThis );
    }

    return cRef;
}

STDMETHODIMP
TestPathMapper_MapPath(
    TestPathMapper * pThis,
    BSTR             bstrConfigPath,
    BSTR             bstrMappedPhysicalPath,
    BSTR *           pbstrNewPhysicalPath
)
{
    BSTR bstrNewPath  = NULL;
    if ( !pThis || pThis->lpVtbl != &vtblTestPathMapper )
    {
        return E_INVALIDARG;
    }

    if( wcscmp( bstrConfigPath, L"MACHINE/WEBROOT/APPHOST" ) == 0 )
    {
        bstrNewPath = SysAllocString( L"%systemdrive%\\inetpub\\temp\\apppools\\DefaultAppPool.config" );
    }
    else
    {
        bstrNewPath = SysAllocString( bstrMappedPhysicalPath );
    }

    if( bstrNewPath == NULL )
    {
        return E_OUTOFMEMORY;
    }

    *pbstrNewPhysicalPath = bstrNewPath;
    return S_OK;
}

int _tmain(
    int argc,
    _TCHAR* argv[]
)
{
    HRESULT   hr        = S_OK;
    DWORD     dwCount   = 0;
    VARIANT   varUnknown;

    TestPathMapper *            pTestPathMapper    = NULL;
    IAppHostAdminManager *      pAMgr              = NULL;
    IAppHostElement *           pElement           = NULL;
    IAppHostElementCollection * pElementCollection = NULL;   

    BSTR bstrPathMapper       = SysAllocString( L"pathMapper" );
    BSTR bstrConfigPath       = SysAllocString( L"MACHINE/WEBROOT/APPHOST" );
    BSTR bstrSitesSectionName = SysAllocString( L"system.webServer/caching" );

    if( bstrPathMapper == NULL || bstrConfigPath == NULL || bstrSitesSectionName == NULL )
    {
        hr = E_OUTOFMEMORY;
        goto Finished;
    }

    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if( FAILED(hr) )
    {
        goto Finished;
    }

    hr = CoCreateInstance( &CLSID_AppHostAdminManager,
                           NULL,
                           CLSCTX_INPROC_SERVER,
                           &IID_IAppHostAdminManager,
                           (void**) &pAMgr );
    if( FAILED( hr ) )
    {
        goto Finished;
    }

    //
    // Create pathMapper structure
    //
    pTestPathMapper = (TestPathMapper *)malloc( sizeof( TestPathMapper ) );
    if( pTestPathMapper == NULL )
    {
        hr = E_OUTOFMEMORY;
        goto Finished;
    }

    pTestPathMapper->lpVtbl = &vtblTestPathMapper;
    pTestPathMapper->cRef   = 1;

    VariantInit(&varUnknown);

    V_VT(&varUnknown)      = VT_UNKNOWN;
    V_UNKNOWN(&varUnknown) = pTestPathMapper;

    //
    // Set pathMapper metadata
    //
    hr = pAMgr->lpVtbl->SetMetadata( pAMgr, bstrPathMapper, varUnknown );
    if( FAILED( hr ) )
    {
        goto Finished;
    }

    //
    // Get the sites section and get number of sites
    //
    hr = pAMgr->lpVtbl->GetAdminSection( pAMgr, bstrSitesSectionName, bstrConfigPath, &pElement );
    if( FAILED( hr ) || pElement == NULL )
    {
        goto Finished;
    }

    hr = pElement->lpVtbl->get_Collection( pElement, &pElementCollection );
    if( FAILED( hr ) || pElementCollection == NULL )
    {
        goto Finished;
    }

    hr = pElementCollection->lpVtbl->get_Count( pElementCollection, &dwCount );
    if( FAILED( hr ) )
    {
        goto Finished;
    }

    wprintf( L"%d\n", dwCount );

Finished:

    VariantClear(&varUnknown);

    if( pElementCollection != NULL )
    {
        pElementCollection->lpVtbl->Release( pElementCollection );
        pElementCollection = NULL;
    }

    if( pElement != NULL )
    {
        pElement->lpVtbl->Release( pElement );
        pElement = NULL;
    }

    if( pAMgr != NULL )
    {
        pAMgr->lpVtbl->Release( pAMgr );
        pAMgr = NULL;
    }

    SysFreeString( bstrPathMapper );
    SysFreeString( bstrConfigPath );
    SysFreeString( bstrSitesSectionName );

    CoUninitialize();
    return 0;
}

Thanks.
Kanwal

New features in configuration system and appcmd in IIS 7.5

Following new features have been added to IIS configuration system and appcmd command line tool in IIS 7.5.

Configuration System

1.       Configuration system tracing and logging
In IIS 7.5, IIS native configuration system can generate trace events capturing all IIS configuration activity. Because all IIS administrative tools (WMI, appcmd, MWA, UI, Powershell etc) call into native configuration system, events are generated irrespective of which administrative tool is used to read/write IIS configuration. Tracing is not enabled by default. You can go to “Application and Service Logs->Microsoft->Windows->IIS-Configuration” in event viewer and enable tracing. IIS generates 4 kinds of events. These are administrative, operational, analytic and debug. Right click on areas in event viewer and select “enable log” for categories you want to enable tracing.

2.       Ability to work with multiple .Net versions
IIS configuration system has the ability to work with multiple .net versions. For this configuration system honors metadata defaultManagedRuntimeVersion. If set, IIS will use this information to find which machine.config and root web.config it should work against. Default version assumed is v2.0. If you want to make configuration system go against other .net version, you are required to set defaultManagedRuntimeVersion metadata. If the configuration path identifies an application, configuration system automatically figures out the version by checking managedRuntimeVersion set for the application pool for the application. All administrative tools are updated to allow working with different versions of NetFx.

3.       Shared configuration now can be configured to use polling instead of change notifications to track changes to applicationHost.config file. This can be configured by specifying “enableUncPolling” and “pollingPeriod” properties in configurationRedirection section. Default value of enableUncPolling is false which means the feature is not turned on by default.

4.        “availableReadableMetadata” and “availableWritableMetadata” metadata properties are available in IAppHostAdminManager, IAppHostElement, IAppHostMethod, IAppHostProperty interfaces. These can be used to find what readable and writable metadata is available for each object.

Appcmd

1. Appcmd now has ability to add <clear/> tag in the collections using ‘~’. Command to clear <error> collection entries in httpErrors section will be following.
appcmd set config /section:httpErrors /~

2. Previously appcmd could only add one type of addElement collection type which limited its utility for authorization section as the addElement could be allow or deny. Now appcmd can be used to add element of a paricular type. Command to add deny entry in authorization section will be as following.
appcmd set config /section:authorization /+deny[verbs=’put’]
appcmd set config /section:authorization /-deny[verbs=’get’]

3. /clr switch can be used to specify which version of .net framework you want to work against when dealing with machine.config and web.config. If this switch is not specified, version dotnet version 2.0 will be assumed.appcmd list config /section:profile /clr:4 (or v4 or 4.0).

4. /admin switch can be used to map configuration path MACHINE/WEBROOT to administration.config. This enables appcmd to be used against administration.config. So command to read moduleProviders section from administration.config will be “appcmd list config /section:moduleProviders /admin”

Hope this helps.
Kanwal

How configuration system merges sections

One of the things which was not clear to me when IIS7 configuration system was written was how configuration system merges all the configuration data available and then decide what values are effective for the current request. One of confusions came from the fact that I assumed that IIS modules try to read the values for the current URL and then keep moving up till it finds the attribute explicitly defined in a configuration file. This is part true for the configuration system but never for the modules. All modules do is ask the configuration system for a merged view of a particular configuration section for a configuration path which usually corresponds to the URL of the requested page. If you are familiar with AhAdmin, this corresponds to the following call.

IAppHostAdminManager::GetAdminSection(<sectionName>, <configPath>);

Section names are full section names as defined in schema.xml and configuration path is of the form “MACHINE/WEBROOT/APPHOST/Default Web Site/Application/VirtualDirectory/Folder/file.php”. Configuration system gives a merged view of the section for the configuration path passed in GetAdminSection call. Modules can ask for a non-merged view of a section as well using IAppHostConfigFile::GetAdminSection but this is not done in any of the modules which is shipped as part of IIS. If invalid configuration is detected (may be because xml is invalid or section is locked at parent level), configuration system returns an error to modules. Modules don’t try to read the configuration at parent level paths if see an error while reading configuration for current URL. Some modules do try to read the configuration at server/site level rather than configuration for current URL path. One reason for this is because some sections are supposed to be “AppHostOnly” and are valid at server level only. HttpCompression and isapiCgiRestrictionList sections are such sections and are marked “AppHostOnly” in the section definitions by IIS setup. If you define these sections at site level or below, configuration system will generate an error. You can get around this error by changing allowDefinition attribute in section definition of these sections but that won’t make modules read the section at levels below server. Another reason why modules sometimes don’t read configuration for current URL is that few properties cannot be varied per URL and are constant for an application pool. An example of such property is system.webServer/caching@maxCacheSize which defines maximum memory size which can be used by caching module. As this property is constant for an application pool, caching module just read this property at server level and not for request URL.

When a merged view of a section is requested, here is what happens in the configuration system. On call to IAppHostAdminManager::GetAdminSection, configuration system reads all configuration files up to the URL path starting with machine.config, root web.config, redirection.config, and applicationHost.config. If allowSubDirConfig property of virtual directory is set to false, configuration system don’t read configuration files below virtual directory paths. As soon as the configuration files are read, few global validations are performed which are done irrespective of which section is requested. If any of global validations fail, configuration system generates an error as soon as it encounters one. Here are the global validations which are made by the configuration system.
   è Schema errors, invalid or duplicate section groups or section definitions are global errors.
  
è Multiple <configSections>, <configProtectedData> elements in applicationHost.config or if they are not the first and second child elements of the root <configuration> in applicationHost.config, it is reported as error.
   
è Errors in sites section are seen when requesting any section as configuration system reads this section to get list of virtual directories.
   
è If configuration system finds a configuration file which doesn’t contain a <configuration> tag or which contains invalid xml, it will result in a configuration error regardless of which section is requested.
  
è If a section for which section definition is missing is used in a configuration file, a configuration error is generated.
  
è A section defined more than once for a configuration path is treated as global error. If none of the global errors are seen by the configuration system, it proceeds to build merged view of the requested section.

Configuration system starts from applicationHost.config and builds a IAppHostConfigElement object based on defaults defined in schema. It then reads the section at root level using IAppHostConfigFile::GetAdminSection and also reads the section under all location paths defined in that file using IAppHostConfigLocation::get_Item and replace existing values with new ones whenever a property which is explicitly defined is seen. If a section level error is encountered a configuration error is generated. Examples of errors which are generated only when requesting a section with error are invalid attribute/element, section locked at parent level, duplicate element in a collection, missing required attributes etc. This makes configuration system report errors at root first followed by errors in sections defined under location paths and then sections in web.config files at lower levels. Child elements and collections are handled similarly. Add/remove/clear for collections gets evaluated from root to the current path. If a duplicate element is found for a collection at any path till current path, an error is generated.

I hope this blog helps you debug configuration errors more easily and understand what to expect from IIS configuration system. This blog post might be little difficult to understand. Let me know if you have some suggestions to make it more readable.

-Kanwal

How to read/write administration.config

IIS7 configuration system understands machine.config, web.config and applicationHost.config but does not handle administration.config natively. This means reading and writing administration.config is little difficult. If you use AhAdmin directly and call GetAdminSection for a section defined in administration.config, it will throw a configuration error for all configuration paths saying that it couldn’t find the section in the configuration file. Easiest way you can work with administration.config is by using Microsoft.Web.Administration (MWA). ServerManager::GetAdministrationConfiguration gives you a Configuration object which represents administration.config. Below is a sample program which uses MWA to print UI module providers registered in administration.config.

class
Program
{
    static void Main(string[] args)
    {
        ServerManager sm = new ServerManager();

        Configuration administrationConfig = sm.GetAdministrationConfiguration();
        ConfigurationSection moduleProvidersSection = administrationConfig.GetSection("moduleProviders");
        ConfigurationElementCollection moduleProvidersCollection = moduleProvidersSection.GetCollection();

        foreach
(ConfigurationElement moduleProviderElement in moduleProvidersCollection)
        {
            Console.WriteLine(moduleProviderElement.GetAttribute("name").Value);
        }
    }
}

MWA achieves this by setting a pathMapper to map MACHINE/WEBROOT configuration path to administration.config instead of root web.config (it uses a different AdminManager for root web.config). You can write a pathMapper yourself and use AppHostAdminLibrary directly to read or write administration.config. Program below uses a simple pathMapper to map MACHINE/WEBROOT to administration.config and then prints UI module count.

using
AppHostAdminLibrary;

class Program
{
    static void Main(string[] args)
    {
        AppHostAdminManager configManager = new AppHostAdminManager();
        configManager.SetMetadata("pathMapper", new MyPathMapper());

        IAppHostElement modulesElement = configManager.GetAdminSection(
            "modules",
            "MACHINE/WEBROOT");
        IAppHostElement wmodulesElement = configManager.GetAdminSection(
            "system.webServer/modules",
            "MACHINE/WEBROOT/APPHOST");

        Console.WriteLine(modulesElement.Collection.Count);
        Console.WriteLine(wmodulesElement.Collection.Count);
    }
}

public
class MyPathMapper : IAppHostPathMapper
{
    public string MapPath(
        string bstrConfigPath,
        string bstrMappedPhysicalPath)
    {
        string physicalPath = bstrMappedPhysicalPath;

        if (bstrConfigPath.Equals("MACHINE/WEBROOT", StringComparison.OrdinalIgnoreCase))
        {
            string windir = Environment.ExpandEnvironmentVariables("%windir%");
            physicalPath = windir + @"\system32\inetsrv\config\administration.config";
        }

        return physicalPath;
    }
}

In windows server 2008 you can also implement IAppHostPathMapper2 and then set “pathMapper2” metadata on IAppHostAdminManager. IAppHostPathMapper2 allows you to return the impersonation token with the physical path mapping which is then used by the configuration system to read the configuration file. Also, native configuration system has an in-built pathMapper which maps MACHINE/WEBROOT to administration.config. Sample program below sets this in-built pathMapper and then creates an IIS manager user.

using
System;
using System.Text;
using System.Security.Cryptography;
using AppHostAdminLibrary;
 namespace AdminConfig
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                AppHostWritableAdminManager ahadmin = new AppHostWritableAdminManager();
                ahadmin.SetMetadata("pathMapper", "AdministrationConfig");
                ahadmin.CommitPath = "MACHINE/WEBROOT";

                IAppHostElement authenticationSection = ahadmin.GetAdminSection(
                    "system.webServer/management/authentication",
                    "MACHINE/WEBROOT");

                IAppHostElement credentialsElement = authenticationSection.GetElementByName("credentials");
                IAppHostElementCollection credentialsCollection = credentialsElement.Collection;
                IAppHostElement newElement = credentialsCollection.CreateNewElement("add");

                newElement.Properties["name"].Value = "newuser";

                //
                // Get SHA256 hash of password
                // This is required by UI
                // Plain text passwords won't work for IIS Manager users
                //
                SHA256 sha = SHA256.Create();
                byte[] hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes("iisrocks"));

                byte f1 = 0xf0;
                byte f2 = 0x0f;
                string hexString = "";

                foreach (byte b in hashBytes)
                {
                    int first4 = (b & f1) >> 4;
                    int second4 = (b & f2);

                    hexString = hexString + ((first4 > 9) ? (char)('A' + (first4 – 10)) : (char)('0' + first4));
                    hexString = hexString + ((second4 > 9) ? ((char)('A' + (second4 – 10))) : (char)('0' + second4));
                }

                newElement.Properties["password"].Value = hexString;

                credentialsCollection.AddElement(newElement, -1);
                ahadmin.CommitChanges();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

You can also use Microsoft.Web.Managerment.Server.ManagementAuthentication.CreateUser("user", "password") to create an IIS Manager user. This will also make sure that user is saved to the right provider which might or might not be administration.config.

 

-Kanwal