Tuesday, May 31, 2011

Setting SSL port bindings in IIS7 with Installshield 2011

Improved code formatting at my new blog: http://growlycode.wordpress.com/2012/03/22/setting-ssl-port-bindings-in-iis7-with-installshield-2011/

Ok, so it's another non-Flex-related post, but I've been doing a lot of .NET work lately, so I'm going to sneak a bit of it in here.

I finally solved the issue I was having setting HTTPS port bindings in InstallShield 2011.

First off, you can't do this via the InstallShield UI for IIS 7. There's an option "SecureBindings" under "Other IIS Properties" in the InstallShield website UI, but this only works for IIS 6. If you want pure, unadulterated IIS 7 support, you need a custom action (CA) that manually sets the port using InstallScript or VBScript.

Caveat emptor!! I am not a windows installer developer. It took me a lot of trial and error to get this going.
If you have a suggestion of a more correct or nicer way of doing things, please leave me a comment.

In the meantime, in the hope that it will save someone else from having to do the same thing, here is what I eventually came up with:

Create an InstallScript CA, called SetSecurePortBindings
This CA will need to be "Deferred" in order to run after the IIS installation is done.
In deferred mode, you won't have access to any non-public installer properties, so if you're setting your ports from properties (e.g. [MYSITE_HTTPS_PORT]) that you set from a config or user dialog input, you'll need to make sure that you store any props you need in your deferred CA in the CustomActionData property for that CA.
I scheduled the custom action After InstallServices but any time after IIS install should be fine. My CA calls the SetSecurePortBindings function.




SetSecurePortBindings then makes a call to ensure that the current IIS version is not IIS 6 (we only support IIS6 and 7). If it is, the function will return, as the values will be set via the SecureBindings entry. If not, the function proceeds to read the site names and port numbers from the CustomDataAction. As I have a number of sites to handle, I have passed a delimited list of properties and am using the string tokeniser function to parse them out into a list.  Then, for each SiteName;PortBinding pair, I call the SetSecurePortBinding(hMSI, siteName, httpsPort) function. 


export prototype SetSecurePortBindings(HWND);
prototype SetSecurePortBinding(HWND, STRING, STRING);
prototype int IISRTGetIISVersion();

/**
 * Sets secure http port bindings using AppCmd.exe. Required for IIS7.
 * This script should be called from a Deferred Custom Action (After InstallServices in my case) and therefore
 * will need to extract any installer properties from the CustomActionData property.
 * The CustomActionData property is populated via a property-set CA (After InstallInitialize),
 * which sets the values of the properties that need to be passed along into a property by the
 * same name as the utilizing CA.
 */
function SetSecurePortBindings(hMSI)
    STRING customActionData, name, httpsPort;
    LIST listProps;
    NUMBER nvSize, nResult, nTokenCount, nCount;
    int nIISVersion;
begin
    SprintfMsiLog("SetSecurePortBindings: %s", "Entered SetSecurePortBindings");   
    if MAINTENANCE != TRUE
    then
        nvSize = 256;
       
        SprintfMsiLog("SetSecurePortBindings: %s", "Non-maintenance operation, proceeding with CA");   

        // If IIS, return, this will be handled by the SecureBindings IIS6 Metabase compatibility
        // module
        nIISVersion = IISRTGetIISVersion() ;
        SprintfMsiLog("SetSecurePortBindings: Detected IIS version '%i'", nIISVersion);   

        if (nIISVersion == 6) then
            SprintfMsiLog("SetSecurePortBindings: IIS6 detected. Returning, as, this will be handled by the SecureBindings IIS6 metabase entry", "");   
            return ERROR_SUCCESS;
        endif;

        // CustomActionData stores the sites and https ports to be configured, in a ; delimited list
        // e.g. Sitename1;*:443:;Sitename2;*:444:
        MsiGetProperty (hMSI, "CustomActionData", customActionData, nvSize);
       
        // Split the properties on the semi-colon delimiter
        listProps = ListCreate (STRINGLIST);
        if (StrGetTokens (listProps, customActionData, ";") > 0) then
            SprintfMsiLog("SetSecurePortBindings: Failed to get tokens from CustomActionData content %s", customActionData);   
        else
       
            nTokenCount = ListCount(listProps);
            SprintfMsiLog("SetSecurePortBindings: Found %i tokens in CustomActionData content %s", nTokenCount, customActionData);   
           
            // Iterate through each block of two (name;port) and call the SetSecurePortBinding method
            nCount = 0;
            while (nCount < nTokenCount)

                if (nCount == 0) then
                    ListGetFirstString(listProps,name);
                else
                    ListGetNextString(listProps,name);
                endif;
                ListGetNextString(listProps,httpsPort);
               
                SetSecurePortBinding(hMSI, name, httpsPort);   
                nCount = nCount + 2;
               
            endwhile;
        endif;
    else
        SprintfMsiLog("SetSecurePortBindings: %s", "Maintenance operation. Skipping...");   
    endif;
end;


/**
 * Returns IIS version detected via registry key
 */
function int IISRTGetIISVersion()
    NUMBER nResult, nVersion, nType, nSize;
    STRING szName, szValue;
begin

    SprintfMsiLog("SetSecurePortBindings: %s", "Attempting to find IIS major version");   

    RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE );

    szName = "MajorVersion";

    nSize = -1;
    nType = REGDB_NUMBER;
    nResult = RegDBGetKeyValueEx("SYSTEM\\CurrentControlSet\\Services\\W3SVC\\Parameters", szName, nType, szValue, nSize);
    if (ISERR_SUCCESS != nResult) then
        return nResult;
    endif;

    nResult = StrToNum(nVersion, szValue);
    if (ISERR_SUCCESS != nResult) then
        return nResult;
    endif;

    RegDBSetDefaultRoot(HKEY_CURRENT_USER );

    return nVersion;
end; 
The SetSecurePortBinding function calls the appcmd.exe util (the new MS-sanctioned way to interact with IIS7) to set the secure port for the named site.

/**
 * Calls AppCmd.exe to set https port bindings
 */
function SetSecurePortBinding(hMSI, svSiteName, svHttpsBinding)
    STRING strMsg, svCall, svCmd, svProg, svParms[500], svOut;
    NUMBER nResult, nLineNum, n;

begin

    SprintfMsiLog("SetSecurePortBindings: Attempting to set port bindings for site '%s' to %s", svSiteName, svHttpsBinding);   

    // IIS 7 setup
    // Check for the correct application pool first
    svCmd = "cmd";
    svProg = " /C " + WINDIR ^ "system32" ^ "inetsrv" ^ "appcmd.exe";
    svCall = " set site /site.name: "+svSiteName+" /+bindings.[protocol='https',bindingInformation='"+svHttpsBinding+"']";   
    svParms = svProg + svCall;
   
    SprintfMsiLog("SetSecurePortBindings: Executing 'cmd %s'", svParms);
    nResult = LaunchAppAndWait(svCmd, svParms, WAIT | LAAW_OPTION_HIDDEN);

    if ( nResult == -1 ) then
        SprintfMsiLog("SetSecurePortBindings: Failed to set set binding. Return value %i", nResult);
        return nResult;
    endif;
   
    SprintfMsiLog("SetSecurePortBindings: %s", "Binding successfully set");
   
    return ERROR_SUCCESS;
end;

3 comments:

  1. Hey growlybear,

    How do i set the CustomActionData Property? I'm having trouble finding out how to do this from within installshield

    ReplyDelete
  2. For those looking for an answer to the above the following link will help...

    http://blogs.flexerasoftware.com/installtalk/2011/06/it-wants-me-to-do-what-some-notes-on-customactiondata-in-installshield.html

    ReplyDelete
  3. Hey growlybear,

    Did you know if its possible to add multiple bindings to an IIS website via Installshield 2018 UI (or at runtime)

    ReplyDelete