// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.

#include "precomp.h"

LPCWSTR vcsFirewallExceptionQuery =
    L"SELECT `Name`, `RemoteAddresses`, `Port`, `Protocol`, `Program`, `Attributes`, `Profile`, `Component_`, `Description`, `Direction` FROM `Wix4FirewallException`";
enum eFirewallExceptionQuery { feqName = 1, feqRemoteAddresses, feqPort, feqProtocol, feqProgram, feqAttributes, feqProfile, feqComponent, feqDescription, feqDirection };
enum eFirewallExceptionTarget { fetPort = 1, fetApplication, fetUnknown };
enum eFirewallExceptionAttributes { feaIgnoreFailures = 1 };

/******************************************************************
 SchedFirewallExceptions - immediate custom action worker to 
   register and remove firewall exceptions.

********************************************************************/
static UINT SchedFirewallExceptions(
    __in MSIHANDLE hInstall,
    WCA_TODO todoSched
    )
{
    HRESULT hr = S_OK;
    UINT er = ERROR_SUCCESS;
    int cFirewallExceptions = 0;

    PMSIHANDLE hView = NULL;
    PMSIHANDLE hRec = NULL;

    LPWSTR pwzCustomActionData = NULL;
    LPWSTR pwzName = NULL;
    LPWSTR pwzRemoteAddresses = NULL;
    LPWSTR pwzPort = NULL;
    int iProtocol = 0;
    int iAttributes = 0;
    int iProfile = 0;
    LPWSTR pwzProgram = NULL;
    LPWSTR pwzComponent = NULL;
    LPWSTR pwzFormattedFile = NULL;
    LPWSTR pwzDescription = NULL;
    int iDirection = MSI_NULL_INTEGER;

    // initialize
    hr = WcaInitialize(hInstall, "SchedFirewallExceptions");
    ExitOnFailure(hr, "Failed to initialize");

    // anything to do?
    if (S_OK != WcaTableExists(L"Wix4FirewallException"))
    {
        WcaLog(LOGMSG_STANDARD, "Wix4FirewallException table doesn't exist, so there are no firewall exceptions to configure.");
        ExitFunction();
    }

    // query and loop through all the firewall exceptions
    hr = WcaOpenExecuteView(vcsFirewallExceptionQuery, &hView);
    ExitOnFailure(hr, "Failed to open view on Wix4FirewallException table");

    while (S_OK == (hr = WcaFetchRecord(hView, &hRec)))
    {
        hr = WcaGetRecordFormattedString(hRec, feqName, &pwzName);
        ExitOnFailure(hr, "Failed to get firewall exception name.");

        hr = WcaGetRecordFormattedString(hRec, feqRemoteAddresses, &pwzRemoteAddresses);
        ExitOnFailure(hr, "Failed to get firewall exception remote addresses.");

        hr = WcaGetRecordFormattedString(hRec, feqPort, &pwzPort);
        ExitOnFailure(hr, "Failed to get firewall exception port.");

        hr = WcaGetRecordInteger(hRec, feqProtocol, &iProtocol);
        ExitOnFailure(hr, "Failed to get firewall exception protocol.");

        hr = WcaGetRecordFormattedString(hRec, feqProgram, &pwzProgram);
        ExitOnFailure(hr, "Failed to get firewall exception program.");

        hr = WcaGetRecordInteger(hRec, feqAttributes, &iAttributes);
        ExitOnFailure(hr, "Failed to get firewall exception attributes.");
        
        hr = WcaGetRecordInteger(hRec, feqProfile, &iProfile);
        ExitOnFailure(hr, "Failed to get firewall exception profile.");

        hr = WcaGetRecordString(hRec, feqComponent, &pwzComponent);
        ExitOnFailure(hr, "Failed to get firewall exception component.");

        hr = WcaGetRecordString(hRec, feqDescription, &pwzDescription);
        ExitOnFailure(hr, "Failed to get firewall exception description.");

        hr = WcaGetRecordInteger(hRec, feqDirection, &iDirection);
        ExitOnFailure(hr, "Failed to get firewall exception direction.");

        // figure out what we're doing for this exception, treating reinstall the same as install
        WCA_TODO todoComponent = WcaGetComponentToDo(pwzComponent);
        if ((WCA_TODO_REINSTALL == todoComponent ? WCA_TODO_INSTALL : todoComponent) != todoSched)
        {
            WcaLog(LOGMSG_STANDARD, "Component '%ls' action state (%d) doesn't match request (%d)", pwzComponent, todoComponent, todoSched);
            continue;
        }

        // action :: name :: profile :: remoteaddresses :: attributes :: target :: {port::protocol | path}
        ++cFirewallExceptions;
        hr = WcaWriteIntegerToCaData(todoComponent, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception action to custom action data");

        hr = WcaWriteStringToCaData(pwzName, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception name to custom action data");

        hr = WcaWriteIntegerToCaData(iProfile, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception profile to custom action data");

        hr = WcaWriteStringToCaData(pwzRemoteAddresses, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception remote addresses to custom action data");

        hr = WcaWriteIntegerToCaData(iAttributes, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception attributes to custom action data");

        if (*pwzProgram)
        {
            // If program is defined, we have an application exception.
            hr = WcaWriteIntegerToCaData(fetApplication, &pwzCustomActionData);
            ExitOnFailure(hr, "failed to write exception target (application) to custom action data");

            hr = WcaWriteStringToCaData(pwzProgram, &pwzCustomActionData);
            ExitOnFailure(hr, "failed to write application path to custom action data");
        }
        else
        {
            // we have a port-only exception
            hr = WcaWriteIntegerToCaData(fetPort, &pwzCustomActionData);
            ExitOnFailure(hr, "failed to write exception target (port) to custom action data");
        }

        hr = WcaWriteStringToCaData(pwzPort, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write application path to custom action data");

        hr = WcaWriteIntegerToCaData(iProtocol, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write exception protocol to custom action data");

        hr = WcaWriteStringToCaData(pwzDescription, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write firewall rule description to custom action data");

        hr = WcaWriteIntegerToCaData(iDirection, &pwzCustomActionData);
        ExitOnFailure(hr, "failed to write firewall rule direction to custom action data");
    }

    // reaching the end of the list is actually a good thing, not an error
    if (E_NOMOREITEMS == hr)
    {
        hr = S_OK;
    } 
    ExitOnFailure(hr, "failure occured while processing Wix4FirewallException table");

    // schedule ExecFirewallExceptions if there's anything to do
    if (pwzCustomActionData && *pwzCustomActionData)
    {
        WcaLog(LOGMSG_STANDARD, "Scheduling firewall exception (%ls)", pwzCustomActionData);

        if (WCA_TODO_INSTALL == todoSched)
        {
            hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"RollbackFirewallExceptionsInstall"), pwzCustomActionData, cFirewallExceptions * COST_FIREWALL_EXCEPTION);
            ExitOnFailure(hr, "failed to schedule firewall install exceptions rollback");            
            hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"ExecFirewallExceptionsInstall"), pwzCustomActionData, cFirewallExceptions * COST_FIREWALL_EXCEPTION);
            ExitOnFailure(hr, "failed to schedule firewall install exceptions execution");
        }
        else
        {
            hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"RollbackFirewallExceptionsUninstall"), pwzCustomActionData, cFirewallExceptions * COST_FIREWALL_EXCEPTION);
            ExitOnFailure(hr, "failed to schedule firewall uninstall exceptions rollback");    
            hr = WcaDoDeferredAction(CUSTOM_ACTION_DECORATION(L"ExecFirewallExceptionsUninstall"), pwzCustomActionData, cFirewallExceptions * COST_FIREWALL_EXCEPTION);
            ExitOnFailure(hr, "failed to schedule firewall uninstall exceptions execution");
        }
    }
    else
    {
        WcaLog(LOGMSG_STANDARD, "No firewall exceptions scheduled");
    }

LExit:
    ReleaseStr(pwzCustomActionData);
    ReleaseStr(pwzName);
    ReleaseStr(pwzRemoteAddresses);
    ReleaseStr(pwzPort);
    ReleaseStr(pwzProgram);
    ReleaseStr(pwzComponent);
    ReleaseStr(pwzDescription);
    ReleaseStr(pwzFormattedFile);

    return WcaFinalize(er = FAILED(hr) ? ERROR_INSTALL_FAILURE : er);
}

/******************************************************************
 SchedFirewallExceptionsInstall - immediate custom action entry
   point to register firewall exceptions.

********************************************************************/
extern "C" UINT __stdcall SchedFirewallExceptionsInstall(
    __in MSIHANDLE hInstall
    )
{
    return SchedFirewallExceptions(hInstall, WCA_TODO_INSTALL);
}

/******************************************************************
 SchedFirewallExceptionsUninstall - immediate custom action entry
   point to remove firewall exceptions.

********************************************************************/
extern "C" UINT __stdcall SchedFirewallExceptionsUninstall(
    __in MSIHANDLE hInstall
    )
{
    return SchedFirewallExceptions(hInstall, WCA_TODO_UNINSTALL);
}

/******************************************************************
 GetFirewallRules - Get the collection of firewall rules.

********************************************************************/
static HRESULT GetFirewallRules(
    __in BOOL fIgnoreFailures,
    __out INetFwRules** ppNetFwRules
    )
{
    HRESULT hr = S_OK;
    INetFwPolicy2* pNetFwPolicy2 = NULL;
    INetFwRules* pNetFwRules = NULL;
    *ppNetFwRules = NULL;
    
    do
    {
        ReleaseNullObject(pNetFwPolicy2);
        ReleaseNullObject(pNetFwRules);

        if (SUCCEEDED(hr = ::CoCreateInstance(__uuidof(NetFwPolicy2), NULL, CLSCTX_ALL, __uuidof(INetFwPolicy2), (void**)&pNetFwPolicy2)) &&
            SUCCEEDED(hr = pNetFwPolicy2->get_Rules(&pNetFwRules)))
        {
            break;
        }
        else if (fIgnoreFailures)
        {
            ExitFunction1(hr = S_FALSE);
        }
        else
        {
            WcaLog(LOGMSG_STANDARD, "Failed to connect to Windows Firewall");
            UINT er = WcaErrorMessage(msierrFirewallCannotConnect, hr, INSTALLMESSAGE_ERROR | MB_ABORTRETRYIGNORE, 0);
            switch (er)
            {
            case IDABORT: // exit with the current HRESULT
                ExitFunction();
            case IDRETRY: // clean up and retry the loop
                hr = S_FALSE;
                break;
            case IDIGNORE: // pass S_FALSE back to the caller, who knows how to ignore the failure
                ExitFunction1(hr = S_FALSE);
            default: // No UI, so default is to fail.
                ExitFunction();
            }
        }
    } while (S_FALSE == hr);

    *ppNetFwRules = pNetFwRules;
    pNetFwRules = NULL;
    
LExit:
    ReleaseObject(pNetFwPolicy2);
    ReleaseObject(pNetFwRules);

    return hr;
}

/******************************************************************
 CreateFwRuleObject - CoCreate a firewall rule, and set the common set of properties which are shared
 between port and application firewall rules

********************************************************************/
static HRESULT CreateFwRuleObject(
    __in BSTR bstrName,
    __in int iProfile,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in LPCWSTR wzDescription,
    __in int iDirection,
    __out INetFwRule** ppNetFwRule
    )
{
    HRESULT hr = S_OK;
    BSTR bstrRemoteAddresses = NULL;
    BSTR bstrPort = NULL;
    BSTR bstrDescription = NULL;
    INetFwRule* pNetFwRule = NULL;
    *ppNetFwRule = NULL;

    // convert to BSTRs to make COM happy
    bstrRemoteAddresses = ::SysAllocString(wzRemoteAddresses);
    ExitOnNull(bstrRemoteAddresses, hr, E_OUTOFMEMORY, "failed SysAllocString for remote addresses");
    bstrPort = ::SysAllocString(wzPort);
    ExitOnNull(bstrPort, hr, E_OUTOFMEMORY, "failed SysAllocString for port");
    bstrDescription = ::SysAllocString(wzDescription);
    ExitOnNull(bstrDescription, hr, E_OUTOFMEMORY, "failed SysAllocString for description");

    hr = ::CoCreateInstance(__uuidof(NetFwRule), NULL, CLSCTX_ALL, __uuidof(INetFwRule), (void**)&pNetFwRule);
    ExitOnFailure(hr, "failed to create NetFwRule object");

    hr = pNetFwRule->put_Name(bstrName);
    ExitOnFailure(hr, "failed to set exception name");

    hr = pNetFwRule->put_Profiles(static_cast<NET_FW_PROFILE_TYPE2>(iProfile));
    ExitOnFailure(hr, "failed to set exception profile");

    if (MSI_NULL_INTEGER != iProtocol)
    {
        hr = pNetFwRule->put_Protocol(static_cast<NET_FW_IP_PROTOCOL>(iProtocol));
        ExitOnFailure(hr, "failed to set exception protocol");
    }

    if (bstrPort && *bstrPort)
    {
        hr = pNetFwRule->put_LocalPorts(bstrPort);
        ExitOnFailure(hr, "failed to set exception port");
    }

    if (bstrRemoteAddresses && *bstrRemoteAddresses)
    {
        hr = pNetFwRule->put_RemoteAddresses(bstrRemoteAddresses);
        ExitOnFailure(hr, "failed to set exception remote addresses '%ls'", bstrRemoteAddresses);
    }

    if (bstrDescription && *bstrDescription)
    {
        hr = pNetFwRule->put_Description(bstrDescription);
        ExitOnFailure(hr, "failed to set exception description '%ls'", bstrDescription);
    }

    if (MSI_NULL_INTEGER != iDirection)
    {
        hr = pNetFwRule->put_Direction(static_cast<NET_FW_RULE_DIRECTION> (iDirection));
        ExitOnFailure(hr, "failed to set exception direction");
    }

    *ppNetFwRule = pNetFwRule;
    pNetFwRule = NULL;

LExit:
    ReleaseBSTR(bstrRemoteAddresses);
    ReleaseBSTR(bstrPort);
    ReleaseBSTR(bstrDescription);
    ReleaseObject(pNetFwRule);

    return hr;
}

/******************************************************************
 FSupportProfiles - Returns true if we support profiles on this machine.
  (Only on Vista or later)

********************************************************************/
static BOOL FSupportProfiles()
{
    BOOL fSupportProfiles = FALSE;
    INetFwRules* pNetFwRules = NULL;

    // We only support profiles if we can co-create an instance of NetFwPolicy2. 
    // This will not work on pre-vista machines.
    if (SUCCEEDED(GetFirewallRules(TRUE, &pNetFwRules)) &&  pNetFwRules != NULL)
    {
        fSupportProfiles = TRUE;
        ReleaseObject(pNetFwRules);
    }

    return fSupportProfiles;
}

/******************************************************************
 GetCurrentFirewallProfile - get the active firewall profile as an
   INetFwProfile, which owns the lists of exceptions we're 
   updating.

********************************************************************/
static HRESULT GetCurrentFirewallProfile(
    __in BOOL fIgnoreFailures,
    __out INetFwProfile** ppfwProfile
    )
{
    HRESULT hr = S_OK;
    INetFwMgr* pfwMgr = NULL;
    INetFwPolicy* pfwPolicy = NULL;
    INetFwProfile* pfwProfile = NULL;
    *ppfwProfile = NULL;
    
    do
    {
        ReleaseNullObject(pfwPolicy);
        ReleaseNullObject(pfwMgr);
        ReleaseNullObject(pfwProfile);

        if (SUCCEEDED(hr = ::CoCreateInstance(__uuidof(NetFwMgr), NULL, CLSCTX_INPROC_SERVER, __uuidof(INetFwMgr), (void**)&pfwMgr)) &&
            SUCCEEDED(hr = pfwMgr->get_LocalPolicy(&pfwPolicy)) &&
            SUCCEEDED(hr = pfwPolicy->get_CurrentProfile(&pfwProfile)))
        {
            break;
        }
        else if (fIgnoreFailures)
        {
            ExitFunction1(hr = S_FALSE);
        }
        else
        {
            WcaLog(LOGMSG_STANDARD, "Failed to connect to Windows Firewall");
            UINT er = WcaErrorMessage(msierrFirewallCannotConnect, hr, INSTALLMESSAGE_ERROR | MB_ABORTRETRYIGNORE, 0);
            switch (er)
            {
            case IDABORT: // exit with the current HRESULT
                ExitFunction();
            case IDRETRY: // clean up and retry the loop
                hr = S_FALSE;
                break;
            case IDIGNORE: // pass S_FALSE back to the caller, who knows how to ignore the failure
                ExitFunction1(hr = S_FALSE);
            default: // No UI, so default is to fail.
                ExitFunction();
            }
        }
    } while (S_FALSE == hr);

    *ppfwProfile = pfwProfile;
    pfwProfile = NULL;
    
LExit:
    ReleaseObject(pfwPolicy);
    ReleaseObject(pfwMgr);
    ReleaseObject(pfwProfile);

    return hr;
}

/******************************************************************
 AddApplicationException

********************************************************************/
static HRESULT AddApplicationException(
    __in LPCWSTR wzFile, 
    __in LPCWSTR wzName,
    __in int iProfile,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in LPCWSTR wzDescription,
    __in int iDirection
    )
{
    HRESULT hr = S_OK;
    BSTR bstrFile = NULL;
    BSTR bstrName = NULL;
    INetFwRules* pNetFwRules = NULL;
    INetFwRule* pNetFwRule = NULL;

    // convert to BSTRs to make COM happy
    bstrFile = ::SysAllocString(wzFile);
    ExitOnNull(bstrFile, hr, E_OUTOFMEMORY, "failed SysAllocString for path");
    bstrName = ::SysAllocString(wzName);
    ExitOnNull(bstrName, hr, E_OUTOFMEMORY, "failed SysAllocString for name");

    // get the collection of firewall rules
    hr = GetFirewallRules(fIgnoreFailures, &pNetFwRules);
    ExitOnFailure(hr, "failed to get firewall rules object");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    // try to find it (i.e., support reinstall)
    hr = pNetFwRules->Item(bstrName, &pNetFwRule);
    if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr)
    {
        hr = CreateFwRuleObject(bstrName, iProfile, wzRemoteAddresses, wzPort, iProtocol, wzDescription, iDirection, &pNetFwRule);
        ExitOnFailure(hr, "failed to create FwRule object");

        // set edge traversal to true
        hr = pNetFwRule->put_EdgeTraversal(VARIANT_TRUE);
        ExitOnFailure(hr, "failed to set application exception edgetraversal property");
        
        // set path
        hr = pNetFwRule->put_ApplicationName(bstrFile);
        ExitOnFailure(hr, "failed to set application name");
        
        // enable it
        hr = pNetFwRule->put_Enabled(VARIANT_TRUE);
        ExitOnFailure(hr, "failed to to enable application exception");
     
        // add it to the list of authorized apps
        hr = pNetFwRules->Add(pNetFwRule);
        ExitOnFailure(hr, "failed to add app to the authorized apps list");
    }
    else
    {
        // we found an existing app exception (if we succeeded, that is)
        ExitOnFailure(hr, "failed trying to find existing app");
        
        // enable it (just in case it was disabled)
        pNetFwRule->put_Enabled(VARIANT_TRUE);
    }

LExit:
    ReleaseBSTR(bstrName);
    ReleaseBSTR(bstrFile);
    ReleaseObject(pNetFwRules);
    ReleaseObject(pNetFwRule);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 AddApplicationExceptionOnCurrentProfile

********************************************************************/
static HRESULT AddApplicationExceptionOnCurrentProfile(
    __in LPCWSTR wzFile, 
    __in LPCWSTR wzName, 
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures
    )
{
    HRESULT hr = S_OK;
    BSTR bstrFile = NULL;
    BSTR bstrName = NULL;
    BSTR bstrRemoteAddresses = NULL;
    INetFwProfile* pfwProfile = NULL;
    INetFwAuthorizedApplications* pfwApps = NULL;
    INetFwAuthorizedApplication* pfwApp = NULL;

    // convert to BSTRs to make COM happy
    bstrFile = ::SysAllocString(wzFile);
    ExitOnNull(bstrFile, hr, E_OUTOFMEMORY, "failed SysAllocString for path");
    bstrName = ::SysAllocString(wzName);
    ExitOnNull(bstrName, hr, E_OUTOFMEMORY, "failed SysAllocString for name");
    bstrRemoteAddresses = ::SysAllocString(wzRemoteAddresses);
    ExitOnNull(bstrRemoteAddresses, hr, E_OUTOFMEMORY, "failed SysAllocString for remote addresses");

    // get the firewall profile, which is our entry point for adding exceptions
    hr = GetCurrentFirewallProfile(fIgnoreFailures, &pfwProfile);
    ExitOnFailure(hr, "failed to get firewall profile");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    // first, let's see if the app is already on the exception list
    hr = pfwProfile->get_AuthorizedApplications(&pfwApps);
    ExitOnFailure(hr, "failed to get list of authorized apps");

    // try to find it (i.e., support reinstall)
    hr = pfwApps->Item(bstrFile, &pfwApp);
    if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr)
    {
        // not found, so we get to add it
        hr = ::CoCreateInstance(__uuidof(NetFwAuthorizedApplication), NULL, CLSCTX_INPROC_SERVER, __uuidof(INetFwAuthorizedApplication), reinterpret_cast<void**>(&pfwApp));
        ExitOnFailure(hr, "failed to create authorized app");

        // set the display name
        hr = pfwApp->put_Name(bstrName);
        ExitOnFailure(hr, "failed to set authorized app name");

        // set path
        hr = pfwApp->put_ProcessImageFileName(bstrFile);
        ExitOnFailure(hr, "failed to set authorized app path");

        // set the allowed remote addresses
        if (bstrRemoteAddresses && *bstrRemoteAddresses)
        {
            hr = pfwApp->put_RemoteAddresses(bstrRemoteAddresses);
            ExitOnFailure(hr, "failed to set authorized app remote addresses");
        }

        // add it to the list of authorized apps
        hr = pfwApps->Add(pfwApp);
        ExitOnFailure(hr, "failed to add app to the authorized apps list");
    }
    else
    {
        // we found an existing app exception (if we succeeded, that is)
        ExitOnFailure(hr, "failed trying to find existing app");

        // enable it (just in case it was disabled)
        pfwApp->put_Enabled(VARIANT_TRUE);
    }

LExit:
    ReleaseBSTR(bstrRemoteAddresses);
    ReleaseBSTR(bstrName);
    ReleaseBSTR(bstrFile);
    ReleaseObject(pfwApp);
    ReleaseObject(pfwApps);
    ReleaseObject(pfwProfile);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 AddPortException

********************************************************************/
static HRESULT AddPortException(
    __in LPCWSTR wzName,
    __in int iProfile,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in LPCWSTR wzDescription,
    __in int iDirection
)
{
    HRESULT hr = S_OK;
    BSTR bstrName = NULL;
    INetFwRules* pNetFwRules = NULL;
    INetFwRule* pNetFwRule = NULL;

    // convert to BSTRs to make COM happy
    bstrName = ::SysAllocString(wzName);
    ExitOnNull(bstrName, hr, E_OUTOFMEMORY, "failed SysAllocString for name");

    // get the collection of firewall rules
    hr = GetFirewallRules(fIgnoreFailures, &pNetFwRules);
    ExitOnFailure(hr, "failed to get firewall rules object");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    // try to find it (i.e., support reinstall)
    hr = pNetFwRules->Item(bstrName, &pNetFwRule);
    if (HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) == hr)
    {
        hr = CreateFwRuleObject(bstrName, iProfile, wzRemoteAddresses, wzPort, iProtocol, wzDescription, iDirection, &pNetFwRule);
        ExitOnFailure(hr, "failed to create FwRule object");

        // enable it
        hr = pNetFwRule->put_Enabled(VARIANT_TRUE);
        ExitOnFailure(hr, "failed to to enable port exception");

        // add it to the list of authorized ports
        hr = pNetFwRules->Add(pNetFwRule);
        ExitOnFailure(hr, "failed to add app to the authorized ports list");
    }
    else
    {
        // we found an existing port exception (if we succeeded, that is)
        ExitOnFailure(hr, "failed trying to find existing port rule");

        // enable it (just in case it was disabled)
        pNetFwRule->put_Enabled(VARIANT_TRUE);
    }

LExit:
    ReleaseBSTR(bstrName);
    ReleaseObject(pNetFwRules);
    ReleaseObject(pNetFwRule);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 AddPortExceptionOnCurrentProfile

********************************************************************/
static HRESULT AddPortExceptionOnCurrentProfile(
    __in LPCWSTR wzName,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures,
    __in int iPort,
    __in int iProtocol
    )
{
    HRESULT hr = S_OK;
    BSTR bstrName = NULL;
    BSTR bstrRemoteAddresses = NULL;
    INetFwProfile* pfwProfile = NULL;
    INetFwOpenPorts* pfwPorts = NULL;
    INetFwOpenPort* pfwPort = NULL;

    // convert to BSTRs to make COM happy
    bstrName = ::SysAllocString(wzName);
    ExitOnNull(bstrName, hr, E_OUTOFMEMORY, "failed SysAllocString for name");
    bstrRemoteAddresses = ::SysAllocString(wzRemoteAddresses);
    ExitOnNull(bstrRemoteAddresses, hr, E_OUTOFMEMORY, "failed SysAllocString for remote addresses");

    // create and initialize a new open port object
    hr = ::CoCreateInstance(__uuidof(NetFwOpenPort), NULL, CLSCTX_INPROC_SERVER, __uuidof(INetFwOpenPort), reinterpret_cast<void**>(&pfwPort));
    ExitOnFailure(hr, "failed to create new open port");

    hr = pfwPort->put_Port(iPort);
    ExitOnFailure(hr, "failed to set exception port");

    hr = pfwPort->put_Protocol(static_cast<NET_FW_IP_PROTOCOL>(iProtocol));
    ExitOnFailure(hr, "failed to set exception protocol");

    if (bstrRemoteAddresses && *bstrRemoteAddresses)
    {
        hr = pfwPort->put_RemoteAddresses(bstrRemoteAddresses);
        ExitOnFailure(hr, "failed to set exception remote addresses '%ls'", bstrRemoteAddresses);
    }

    hr = pfwPort->put_Name(bstrName);
    ExitOnFailure(hr, "failed to set exception name");

    // get the firewall profile, its current list of open ports, and add ours
    hr = GetCurrentFirewallProfile(fIgnoreFailures, &pfwProfile);
    ExitOnFailure(hr, "failed to get firewall profile");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    hr = pfwProfile->get_GloballyOpenPorts(&pfwPorts);
    ExitOnFailure(hr, "failed to get open ports");

    hr = pfwPorts->Add(pfwPort);
    ExitOnFailure(hr, "failed to add exception to global list");

LExit:
    ReleaseBSTR(bstrRemoteAddresses);
    ReleaseBSTR(bstrName);
    ReleaseObject(pfwProfile);
    ReleaseObject(pfwPorts);
    ReleaseObject(pfwPort);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 RemoveException - Removes the exception rule with the given name.

********************************************************************/
static HRESULT RemoveException(
    __in LPCWSTR wzName, 
    __in BOOL fIgnoreFailures
    )
{
    HRESULT hr = S_OK;;
    INetFwRules* pNetFwRules = NULL;

    // convert to BSTRs to make COM happy
    BSTR bstrName = ::SysAllocString(wzName);
    ExitOnNull(bstrName, hr, E_OUTOFMEMORY, "failed SysAllocString for path");

    // get the collection of firewall rules
    hr = GetFirewallRules(fIgnoreFailures, &pNetFwRules);
    ExitOnFailure(hr, "failed to get firewall rules object");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    hr = pNetFwRules->Remove(bstrName);
    ExitOnFailure(hr, "failed to remove authorized app");

LExit:
    ReleaseBSTR(bstrName);
    ReleaseObject(pNetFwRules);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 RemoveApplicationExceptionFromCurrentProfile

********************************************************************/
static HRESULT RemoveApplicationExceptionFromCurrentProfile(
    __in LPCWSTR wzFile, 
    __in BOOL fIgnoreFailures
    )
{
    HRESULT hr = S_OK;
    INetFwProfile* pfwProfile = NULL;
    INetFwAuthorizedApplications* pfwApps = NULL;

    // convert to BSTRs to make COM happy
    BSTR bstrFile = ::SysAllocString(wzFile);
    ExitOnNull(bstrFile, hr, E_OUTOFMEMORY, "failed SysAllocString for path");

    // get the firewall profile, which is our entry point for removing exceptions
    hr = GetCurrentFirewallProfile(fIgnoreFailures, &pfwProfile);
    ExitOnFailure(hr, "failed to get firewall profile");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    // now get the list of app exceptions and remove the one
    hr = pfwProfile->get_AuthorizedApplications(&pfwApps);
    ExitOnFailure(hr, "failed to get list of authorized apps");

    hr = pfwApps->Remove(bstrFile);
    ExitOnFailure(hr, "failed to remove authorized app");

LExit:
    ReleaseBSTR(bstrFile);
    ReleaseObject(pfwApps);
    ReleaseObject(pfwProfile);

    return fIgnoreFailures ? S_OK : hr;
}

/******************************************************************
 RemovePortExceptionFromCurrentProfile

********************************************************************/
static HRESULT RemovePortExceptionFromCurrentProfile(
    __in int iPort,
    __in int iProtocol,
    __in BOOL fIgnoreFailures
    )
{
    HRESULT hr = S_OK;
    INetFwProfile* pfwProfile = NULL;
    INetFwOpenPorts* pfwPorts = NULL;

    // get the firewall profile, which is our entry point for adding exceptions
    hr = GetCurrentFirewallProfile(fIgnoreFailures, &pfwProfile);
    ExitOnFailure(hr, "failed to get firewall profile");
    if (S_FALSE == hr) // user or package author chose to ignore missing firewall
    {
        ExitFunction();
    }

    hr = pfwProfile->get_GloballyOpenPorts(&pfwPorts);
    ExitOnFailure(hr, "failed to get open ports");

    hr = pfwPorts->Remove(iPort, static_cast<NET_FW_IP_PROTOCOL>(iProtocol));
    ExitOnFailure(hr, "failed to remove open port %d, protocol %d", iPort, iProtocol);

LExit:
    return fIgnoreFailures ? S_OK : hr;
}

static HRESULT AddApplicationException(
    __in BOOL fSupportProfiles,
    __in LPCWSTR wzFile, 
    __in LPCWSTR wzName,
    __in int iProfile,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in LPCWSTR wzDescription,
    __in int iDirection
)
{
    HRESULT hr = S_OK;

    if (fSupportProfiles)
    {
        hr = AddApplicationException(wzFile, wzName, iProfile, wzRemoteAddresses, fIgnoreFailures, wzPort, iProtocol, wzDescription, iDirection);
    }
    else
    {
        if (0 != *wzPort || MSI_NULL_INTEGER != iProtocol)
        {
            // NOTE: This is treated as an error rather than either creating a rule based on just the application (no port), or 
            // just the port because it is unclear what is the proper fall back. For example, suppose that you have code that 
            // runs in dllhost.exe. Clearly falling back to opening all of dllhost is wrong. Because the firewall is a security
            // feature, it seems better to require the MSI author to indicate the behavior that they want.
            WcaLog(LOGMSG_STANDARD, "FirewallExtension: Cannot add firewall rule '%ls', which defines both an application and a port or protocol. Such a rule requires Microsoft Windows Vista or later.", wzName);
            return fIgnoreFailures ? S_OK : E_NOTIMPL;
        }

        hr = AddApplicationExceptionOnCurrentProfile(wzFile, wzName, wzRemoteAddresses, fIgnoreFailures);
    }

    return hr;
}

static HRESULT AddPortException(
    __in BOOL fSupportProfiles,
    __in LPCWSTR wzName,
    __in int iProfile,
    __in_opt LPCWSTR wzRemoteAddresses,
    __in BOOL fIgnoreFailures,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in LPCWSTR wzDescription,
    __in int iDirection
)
{
    HRESULT hr = S_OK;

    if (fSupportProfiles)
    {
        hr = AddPortException(wzName, iProfile, wzRemoteAddresses, fIgnoreFailures, wzPort, iProtocol, wzDescription, iDirection);
    }
    else
    {
        hr = AddPortExceptionOnCurrentProfile(wzName, wzRemoteAddresses, fIgnoreFailures, wcstol(wzPort, NULL, 10), iProtocol);
    }

    return hr;
}

static HRESULT RemoveApplicationException(
    __in BOOL fSupportProfiles,
    __in LPCWSTR wzName,
    __in LPCWSTR wzFile, 
    __in BOOL fIgnoreFailures,
    __in LPCWSTR wzPort,
    __in int iProtocol
    )
{
    HRESULT hr = S_OK;

    if (fSupportProfiles)
    {
        hr = RemoveException(wzName, fIgnoreFailures);
    }
    else
    {
        if (0 != *wzPort || MSI_NULL_INTEGER != iProtocol)
        {
            WcaLog(LOGMSG_STANDARD, "FirewallExtension: Cannot remove firewall rule '%ls', which defines both an application and a port or protocol. Such a rule requires Microsoft Windows Vista or later.", wzName);
            return S_OK;
        }

        hr = RemoveApplicationExceptionFromCurrentProfile(wzFile, fIgnoreFailures);
    }

    return hr;
}

static HRESULT RemovePortException(
    __in BOOL fSupportProfiles,
    __in LPCWSTR wzName,
    __in LPCWSTR wzPort,
    __in int iProtocol,
    __in BOOL fIgnoreFailures
    )
{
    HRESULT hr = S_OK;

    if (fSupportProfiles)
    {
        hr = RemoveException(wzName, fIgnoreFailures);
    }
    else
    {
        hr = RemovePortExceptionFromCurrentProfile(wcstol(wzPort, NULL, 10), iProtocol, fIgnoreFailures);
    }

    return hr;
}

/******************************************************************
 ExecFirewallExceptions - deferred custom action entry point to 
   register and remove firewall exceptions.

********************************************************************/
extern "C" UINT __stdcall ExecFirewallExceptions(
    __in MSIHANDLE hInstall
    )
{
    HRESULT hr = S_OK;
    BOOL fSupportProfiles = FALSE;
    LPWSTR pwz = NULL;
    LPWSTR pwzCustomActionData = NULL;
    int iTodo = WCA_TODO_UNKNOWN;
    LPWSTR pwzName = NULL;
    LPWSTR pwzRemoteAddresses = NULL;
    int iAttributes = 0;
    int iTarget = fetUnknown;
    LPWSTR pwzFile = NULL;
    LPWSTR pwzPort = NULL;
    LPWSTR pwzDescription = NULL;
    int iProtocol = 0;
    int iProfile = 0;
    int iDirection = 0;

    // initialize
    hr = WcaInitialize(hInstall, "ExecFirewallExceptions");
    ExitOnFailure(hr, "failed to initialize");

    hr = WcaGetProperty( L"CustomActionData", &pwzCustomActionData);
    ExitOnFailure(hr, "failed to get CustomActionData");
    WcaLog(LOGMSG_TRACEONLY, "CustomActionData: %ls", pwzCustomActionData);

    hr = ::CoInitialize(NULL);
    ExitOnFailure(hr, "failed to initialize COM");

    // Find out if we support profiles (only on Vista or later)
    fSupportProfiles = FSupportProfiles();

    // loop through all the passed in data
    pwz = pwzCustomActionData;
    while (pwz && *pwz)
    {
        // extract the custom action data and if rolling back, swap INSTALL and UNINSTALL
        hr = WcaReadIntegerFromCaData(&pwz, &iTodo);
        ExitOnFailure(hr, "failed to read todo from custom action data");
        if (::MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK))
        {
            if (WCA_TODO_INSTALL == iTodo)
            {
                iTodo = WCA_TODO_UNINSTALL;
            }
            else if (WCA_TODO_UNINSTALL == iTodo)
            {
                iTodo = WCA_TODO_INSTALL;
            }
        }

        hr = WcaReadStringFromCaData(&pwz, &pwzName);
        ExitOnFailure(hr, "failed to read name from custom action data");

        hr = WcaReadIntegerFromCaData(&pwz, &iProfile);
        ExitOnFailure(hr, "failed to read profile from custom action data");

        hr = WcaReadStringFromCaData(&pwz, &pwzRemoteAddresses);
        ExitOnFailure(hr, "failed to read remote addresses from custom action data");

        hr = WcaReadIntegerFromCaData(&pwz, &iAttributes);
        ExitOnFailure(hr, "failed to read attributes from custom action data");
        BOOL fIgnoreFailures = feaIgnoreFailures == (iAttributes & feaIgnoreFailures);

        hr = WcaReadIntegerFromCaData(&pwz, &iTarget);
        ExitOnFailure(hr, "failed to read target from custom action data");

        if (iTarget == fetApplication)
        {
            hr = WcaReadStringFromCaData(&pwz, &pwzFile);
            ExitOnFailure(hr, "failed to read file path from custom action data");
        }

        hr = WcaReadStringFromCaData(&pwz, &pwzPort);
        ExitOnFailure(hr, "failed to read port from custom action data");
        hr = WcaReadIntegerFromCaData(&pwz, &iProtocol);
        ExitOnFailure(hr, "failed to read protocol from custom action data");
        hr = WcaReadStringFromCaData(&pwz, &pwzDescription);
        ExitOnFailure(hr, "failed to read protocol from custom action data");
        hr = WcaReadIntegerFromCaData(&pwz, &iDirection);
        ExitOnFailure(hr, "failed to read direction from custom action data");

        switch (iTarget)
        {
        case fetPort:
            switch (iTodo)
            {
            case WCA_TODO_INSTALL:
            case WCA_TODO_REINSTALL:
                WcaLog(LOGMSG_STANDARD, "Installing firewall exception2 %ls on port %ls, protocol %d", pwzName, pwzPort, iProtocol);
                hr = AddPortException(fSupportProfiles, pwzName, iProfile, pwzRemoteAddresses, fIgnoreFailures, pwzPort, iProtocol, pwzDescription, iDirection);
                ExitOnFailure(hr, "failed to add/update port exception for name '%ls' on port %ls, protocol %d", pwzName, pwzPort, iProtocol);
                break;

            case WCA_TODO_UNINSTALL:
                WcaLog(LOGMSG_STANDARD, "Uninstalling firewall exception2 %ls on port %ls, protocol %d", pwzName, pwzPort, iProtocol);
                hr = RemovePortException(fSupportProfiles, pwzName, pwzPort, iProtocol, fIgnoreFailures);
                ExitOnFailure(hr, "failed to remove port exception for name '%ls' on port %ls, protocol %d", pwzName, pwzPort, iProtocol);
                break;
            }
            break;

        case fetApplication:
            switch (iTodo)
            {
            case WCA_TODO_INSTALL:
            case WCA_TODO_REINSTALL:
                WcaLog(LOGMSG_STANDARD, "Installing firewall exception2 %ls (%ls)", pwzName, pwzFile);
                hr = AddApplicationException(fSupportProfiles, pwzFile, pwzName, iProfile, pwzRemoteAddresses, fIgnoreFailures, pwzPort, iProtocol, pwzDescription, iDirection);
                ExitOnFailure(hr, "failed to add/update application exception for name '%ls', file '%ls'", pwzName, pwzFile);
                break;

            case WCA_TODO_UNINSTALL:
                WcaLog(LOGMSG_STANDARD, "Uninstalling firewall exception2 %ls (%ls)", pwzName, pwzFile);
                hr = RemoveApplicationException(fSupportProfiles, pwzName, pwzFile, fIgnoreFailures, pwzPort, iProtocol);
                ExitOnFailure(hr, "failed to remove application exception for name '%ls', file '%ls'", pwzName, pwzFile);
                break;
            }
            break;
        }
    }

LExit:
    ReleaseStr(pwzCustomActionData);
    ReleaseStr(pwzName);
    ReleaseStr(pwzRemoteAddresses);
    ReleaseStr(pwzFile);
    ReleaseStr(pwzPort);
    ReleaseStr(pwzDescription);
    ::CoUninitialize();

    return WcaFinalize(FAILED(hr) ? ERROR_INSTALL_FAILURE : ERROR_SUCCESS);
}