Damian Mehers' Blog Android, VR and Wearables from Geneva, Switzerland.

21Feb/086

Configuring HTTP Namespace reservations on Vista using WIX

WIX is an XML based installer generator, from Microsoft, now hosted in Sourceforge.

As part of an installation for a Vista based product I am creating, I needed to register an http namespace.  This is required in order to listen for incoming connections.  You can do this from the command line as described here: http://msdn2.microsoft.com/en-us/library/ms733768.aspx

I spent a day or so researching, and coding a Custom Action, which worked.  As I was in the final stages of integrating it into my WIX wxs file, I discovered a much simpler way of doing it, based on the way Microsoft Media Center applications are registered in the template code created in Visual Studio 2008 by the Windows Media Center SDK.

The first thing I did was to define a property which points to the fully qualified Netsh.exe tool:

<!-- This property uses an AppSearch to attempt to  -->
<!-- locate the file %windir\Netsh.exe              -->
<Property Id="NETSH" Secure="yes">
  <DirectorySearch Id="Netshdir" Path="[SystemFolder]">
    <FileSearch Id="Netshexe" Name="Netsh.exe" />
  </DirectorySearch>
</Property>

Then I defined a couple of properties that describe the Netsh commands to register and unregister the HTTP namespace.  You'd need to replace the port, 'Test' and possibly specify something else instead of 'Users':

<Property Id="NamespaceAddCmd"
          Value="http add urlacl url=http://+:123/Test/ user=Users"/>
<Property Id="NamespaceDelCmd"
          Value="http del urlacl url=http://+:123/Test/"/>

Next I define the custom actions used when installing and uninstalling. These are based on those used for registering MCE apps. The '..._Cmd' actions set properties which are then used by the actions they target. This is because deferred actions can not access normal properties, only the CustomActionData property.  This is set by the '..._Cmd' actions when they have a Property which names the custom action whose CustomActionData they target:

<CustomAction Id="CA_RegisterNamespace_Unregister_Install_Cmd"
              Property="CA_RegisterNamespace_Unregister_Install"
              Value="&quot;[NETSH]&quot; [NamespaceDelCmd]"/>
<CustomAction Id="CA_RegisterNamespace_Unregister_Uninstall_Cmd"
              Property="CA_RegisterNamespace_Unregister_Uninstall"
              Value="&quot;[NETSH]&quot; [NamespaceDelCmd]"/>
<CustomAction Id="CA_RegisterNamespace_Register_Cmd"
              Property="CA_RegisterNamespace_Register"
              Value="&quot;[NETSH]&quot; [NamespaceAddCmd]"/>
<CustomAction Id="CA_RegisterNamespace_Rollback_Cmd"
              Property="CA_RegisterNamespace_Rollback"
              Value="&quot;[NETSH]&quot; [NamespaceDelCmd]"/>

<CustomAction Id="CA_RegisterNamespace_Unregister_Install"
              BinaryKey="WixCA" DllEntry="CAQuietExec"
              Execute="deferred" Return="ignore" Impersonate="no"/>
<CustomAction Id="CA_RegisterNamespace_Unregister_Uninstall"
              BinaryKey="WixCA" DllEntry="CAQuietExec"
              Execute="deferred" Return="ignore" Impersonate="no"/>
<CustomAction Id="CA_RegisterNamespace_Register"
              BinaryKey="WixCA" DllEntry="CAQuietExec"
              Execute="deferred" Return="check" Impersonate="no"/>
<CustomAction Id="CA_RegisterNamespace_Rollback"
              BinaryKey="WixCA" DllEntry="CAQuietExec"
              Execute="rollback" Return="ignore" Impersonate="no"/>

Finally, these are the custom actions used in the InstallExecuteSequence:

<Custom Action="CA_RegisterNamespace_Unregister_Install_Cmd"
        After="CostFinalize">
  <![CDATA[
NOT REMOVE]]>
</
Custom>
<
Custom Action="CA_RegisterNamespace_Unregister_Uninstall_Cmd"
        After="CA_RegisterNamespace_Unregister_Install_Cmd">
  <![CDATA[
REMOVE AND ($Registration.xml = 2)]]>
</
Custom>
<
Custom Action="CA_RegisterNamespace_Register_Cmd"
        After="CA_RegisterNamespace_Unregister_Uninstall_Cmd">
  <![CDATA[
NOT REMOVE]]>
</
Custom>
<
Custom Action="CA_RegisterNamespace_Rollback_Cmd"
        After="CA_RegisterNamespace_Register_Cmd">
  <![CDATA[
NOT REMOVE]]>
</
Custom>

<Custom Action="CA_RegisterNamespace_Unregister_Uninstall"
        Before="RemoveFiles">
  <![CDATA[
REMOVE AND ($Registration.xml = 2)]]>
</
Custom>

<Custom Action="CA_RegisterNamespace_Rollback"
        After="InstallFiles">
  <![CDATA[
NOT REMOVE]]>
</
Custom>
<
Custom Action="CA_RegisterNamespace_Unregister_Install"
        After="CA_RegisterNamespace_Rollback">
  <![CDATA[
NOT REMOVE]]>
</
Custom>
<
Custom Action="CA_RegisterNamespace_Register"
        After="CA_RegisterNamespace_Unregister_Install">
  <![CDATA[
NOT REMOVE]]>
</
Custom>

Just in case it is of use to anyone, here is the custom action I coded up, although it is not needed with the above code:

#include <windows.h>
#include <msi.h>
#include <msiquery.h>
#include <Http.h>
#include <Sddl.h>

#pragma comment(linker, "/EXPORT:ReserveNamespace=_ReserveNamespace@4")
#pragma comment(linker, "/EXPORT:ReleaseNamespace=_ReleaseNamespace@4")

#define BUF_LEN 256

#define LOG true

UINT ReserveOrReleaseNamespace (MSIHANDLE hInstall, bool reserve) {
    HWND parentWindow = GetForegroundWindow();

    if(LOG) MessageBox(parentWindow, L"Called", L"ReserveOrReleaseNamespace", MB_OK);

    bool scheduled = MsiGetMode(hInstall, MSIRUNMODE_SCHEDULED);
    bool rollback = MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK);
    bool commit = MsiGetMode(hInstall, MSIRUNMODE_COMMIT);

    if(LOG) MessageBox(parentWindow, scheduled ? L"True" : L"False",
        L"ReserveOrReleaseNamespace is MSIRUNMODE_SCHEDULED", MB_OK);
    if(LOG) MessageBox(parentWindow, rollback ? L"True" : L"False",
        L"ReserveOrReleaseNamespace is MSIRUNMODE_ROLLBACK", MB_OK);
    if(LOG) MessageBox(parentWindow, commit ? L"True" : L"False",
        L"ReserveOrReleaseNamespace is MSIRUNMODE_COMMIT", MB_OK);

    // Initialize HTTP
    HTTPAPI_VERSION httpApiVersion = HTTPAPI_VERSION_1;
    HRESULT error = HttpInitialize(httpApiVersion, HTTP_INITIALIZE_CONFIG, 0);
    if(error != S_OK) return error;

    // Get the custom action data
    // (only thing we can pass to deferred custom actions)
    wchar_t customActionData[BUF_LEN] = L"";
    DWORD customActionDataLen = BUF_LEN;

    MsiGetProperty (hInstall, L"CustomActionData", customActionData,
        &customActionDataLen);
    if(LOG) MessageBox(parentWindow, customActionData,
        L"ReserveOrReleaseNamespace CustomActionData is", MB_OK);

    wchar_t* semicolon = wcschr(customActionData, ';');
    if(!semicolon) {
        MessageBox(parentWindow,
            L"MceFM Namespace Reservation failed because the CustomActionData was incorrect",
            L"MceFM Namespace Reservation", MB_OK);
        return E_FAIL;
    }

    // Extract the user and URL
    wchar_t* user = customActionData;
    wchar_t* url = semicolon + 1;

    *semicolon = '';

    if(LOG) MessageBox(parentWindow, url, L"ReserveOrReleaseNamespace NR_URL is", MB_OK);
    if(LOG) MessageBox(parentWindow, user, L"ReserveOrReleaseNamespace NR_USER is", MB_OK);

    // Convert the user to a SID
    PSID pSID;
    DWORD sidSize = SECURITY_MAX_SID_SIZE;
    pSID = LocalAlloc(LMEM_FIXED, sidSize);
    WCHAR domainName[BUF_LEN];
    DWORD domainNameLen = BUF_LEN;
    SID_NAME_USE sidNameUse;
    BOOL worked = LookupAccountName(NULL, user, pSID, &sidSize, domainName,
        &domainNameLen, &sidNameUse);
    if(!worked) {
        error = GetLastError();
        if(LOG) MessageBox(parentWindow, L"LookupAccountName failed",
            L"ReserveOrReleaseNamespace", MB_OK);
        return error;
    }

    // Convert the SID to a string
    LPTSTR stringSid;
    worked = ConvertSidToStringSid(pSID, &stringSid);
    if(!worked) {
        error = GetLastError();
        if(LOG) MessageBox(parentWindow, L"ConvertSidToStringSid failed",
            L"ReserveOrReleaseNamespace", MB_OK);
        LocalFree(pSID);
        return error;
    }


    // Build a security descriptor in the required format
    WCHAR securityDescriptor[BUF_LEN];
    wsprintf(securityDescriptor, L"D:(A;;GX;;;%s)", stringSid);

    // Set up the config info (url and security descriptor)
    HTTP_SERVICE_CONFIG_URLACL_SET configInfo;
    configInfo.KeyDesc.pUrlPrefix = url;
    configInfo.ParamDesc.pStringSecurityDescriptor =  securityDescriptor;

    if(reserve) {
        // Add the namespace reservation
        error = HttpSetServiceConfiguration(0, HttpServiceConfigUrlAclInfo,
            &configInfo, sizeof(HTTP_SERVICE_CONFIG_URLACL_SET), NULL);
        if(error != S_OK) {
            if(LOG) MessageBox(parentWindow, L"HttpSetServiceConfiguration failed",
                L"ReserveOrReleaseNamespace", MB_OK);
        }
    } else {
        // Remove the namespace reservation
        error = HttpDeleteServiceConfiguration(0,  HttpServiceConfigUrlAclInfo,
            &configInfo, sizeof(HTTP_SERVICE_CONFIG_URLACL_SET), NULL);
        if(error != S_OK) {
            if(LOG) MessageBox(parentWindow, L"HttpDeleteServiceConfiguration failed",
                L"ReserveOrReleaseNamespace", MB_OK);
        }
    }
    LocalFree(pSID);
    LocalFree(stringSid);

    return error;
}



extern "C" UINT __stdcall ReserveNamespace (MSIHANDLE hInstall) {
    return ReserveOrReleaseNamespace(hInstall, true);
}

extern "C" UINT __stdcall ReleaseNamespace (MSIHANDLE hInstall) {
    return ReserveOrReleaseNamespace(hInstall, false);
}

Comments (6) Trackbacks (0)
  1. This doesn’t work, I get error 1721 when it tries to run the netsh from custom action.

  2. Works perfectly for me, thanks!

  3. Doesn’t quite work for me – checking the MSIEXEC logs, it gets as far as the following entries:

    Action 12:07:54: InstallFiles. Copying new files
    Action 12:07:54: CA_RegisterNamespace_Rollback.
    Action 12:07:54: CA_RegisterNamespace_Unregister_Install.

    And then just sits there. In Process Explorer, I can see that msiexec has invoked NETSH, but it just sits there and does nothing (I’ve tried leaving it for up to 30 minutes, without anything happening). Any ideas?

  4. I know this is an old post, but I had the same problem as David Keaveny. In the end, it seems that changing the property names of NamespaceAddCmd and NamespaceDelCmd to all caps (i.e. NAMESPACE_ADD_CMD and NAMESPACE_DEL_CMD) resolved this problem for me. Before this change, these two properties were empty to the *_Cmd custom actions, which resulted in netsh being run without parameters (interactive mode).

  5. Thanks Filip – it’s been 5 years since I wrote this post, so I’m glad to see it is still being read & used. I appreciate you adding this info.

  6. This is the only approach I could get to work and it works great. I used it to execute the netsh.exe to bind an ssl cert to a port for a self-hosted service. Thanks.


Leave a comment

No trackbacks yet.