DSCResources/MSFT_xDSCWebService/PSWSIISEndpoint.psm1

# This module file contains a utility to perform PSWS IIS Endpoint setup
# Module exports New-PSWSEndpoint function to perform the endpoint setup

# Name and description for the Firewall rules. Used in multiple locations
$FireWallRuleDisplayName = 'Desired State Configuration - Pull Server Port:{0}'

<#
    .SYNOPSIS
        Validate supplied configuration to setup the PSWS Endpoint Function
        checks for the existence of PSWS Schema files, IIS config Also validate
        presence of IIS on the target machine
#>

function Initialize-Endpoint
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $site,

        [Parameter()]
        [System.String]
        $path,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $cfgfile,

        [Parameter()]
        [System.Int32]
        $port,

        [Parameter()]
        [System.String]
        $app,

        [Parameter()]
        [System.String]
        $applicationPoolIdentityType,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $svc,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $mof,

        [Parameter()]
        [System.String]
        $dispatch,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $asax,

        [Parameter()]
        [System.String[]]
        $dependentBinaries,

        [Parameter()]
        [System.String]
        $language,

        [Parameter()]
        [System.String[]]
        $dependentMUIFiles,

        [Parameter()]
        [System.String[]]
        $psFiles,

        [Parameter()]
        [System.Boolean]
        $removeSiteFiles = $false,

        [Parameter()]
        [System.String]
        $certificateThumbPrint,

        [Parameter()]
        [System.Boolean]
        $enable32BitAppOnWin64
    )

    if ($certificateThumbPrint -ne 'AllowUnencryptedTraffic')
    {
        Write-Verbose -Message 'Verify that the certificate with the provided thumbprint exists in CERT:\LocalMachine\MY\'

        $certificate = Get-ChildItem -Path CERT:\LocalMachine\MY\ | Where-Object -FilterScript {
            $_.Thumbprint -eq $certificateThumbPrint
        }

        if (!$Certificate)
        {
             throw "ERROR: Certificate with thumbprint $certificateThumbPrint does not exist in CERT:\LocalMachine\MY\"
        }
    }

    Test-IISInstall

    $appPool = 'PSWS'

    Write-Verbose -Message 'Delete the App Pool if it exists'
    Remove-AppPool -apppool $appPool

    Write-Verbose -Message 'Remove the site if it already exists'
    Update-Site -siteName $site -siteAction Remove

    # Check for existing binding, there should be no binding with the same port
    $allWebBindingsOnPort = Get-WebBinding | Where-Object -FilterScript {
        $_.BindingInformation -eq "*:$($port):"
    }

    if ($allWebBindingsOnPort.Count -gt 0)
    {
        throw "ERROR: Port $port is already used, please review existing sites and change the port to be used."
    }

    if ($removeSiteFiles)
    {
        if(Test-Path -Path $path)
        {
            Remove-Item -Path $path -Recurse -Force
        }
    }

    Copy-PSWSConfigurationToIISEndpointFolder -path $path -cfgfile $cfgfile -svc $svc -mof $mof -dispatch $dispatch -asax $asax -dependentBinaries $dependentBinaries -language $language -dependentMUIFiles $dependentMUIFiles -psFiles $psFiles

    New-IISWebSite -site $site -path $path -port $port -app $app -apppool $appPool -applicationPoolIdentityType $applicationPoolIdentityType -certificateThumbPrint $certificateThumbPrint -enable32BitAppOnWin64 $enable32BitAppOnWin64
}

<#
    .SYNOPSIS
        Validate if IIS and all required dependencies are installed on the
        target machine
#>

function Test-IISInstall
{
    [CmdletBinding()]
    param()

    Write-Verbose -Message 'Checking IIS requirements'
    $iisVersion = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\InetStp -ErrorAction silentlycontinue).MajorVersion

    if ($iisVersion -lt 7)
    {
        throw "ERROR: IIS Version detected is $iisVersion , must be running higher than 7.0"
    }

    $wsRegKey = (Get-ItemProperty hklm:\SYSTEM\CurrentControlSet\Services\W3SVC -ErrorAction silentlycontinue).ImagePath
    if ($null -eq $wsRegKey)
    {
        throw 'ERROR: Cannot retrive W3SVC key. IIS Web Services may not be installed'
    }

    if ((Get-Service w3svc).Status -ne 'running')
    {
        throw 'ERROR: service W3SVC is not running'
    }
}

<#
    .SYNOPSIS
        Verify if a given IIS Site exists
#>

function Test-ForIISSite
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter()]
        [System.String]
        $siteName
    )

    if (Get-Website -Name $siteName)
    {
        return $true
    }

    return $false
}

<#
    .SYNOPSIS
        Perform an action (such as stop, start, delete) for a given IIS Site
#>

function Update-Site
{
    param
    (
        [Parameter(ParameterSetName = 'SiteName', Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $siteName,

        [Parameter(ParameterSetName = 'Site', Mandatory = $true, Position = 0)]
        [System.Object]
        $site,

        [Parameter(ParameterSetName = 'SiteName', Mandatory = $true, Position = 1)]
        [Parameter(ParameterSetName = 'Site', Mandatory = $true, Position = 1)]
        [System.String]
        $siteAction
    )

    [System.String] $name = $null

    if ($PSCmdlet.ParameterSetName -eq 'SiteName')
    {
        $name = $siteName
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'Site')
    {
        $name = $site.Name
    }

    if (Test-ForIISSite -siteName $name)
    {
        switch ($siteAction)
        {
            'Start'  {Start-Website -Name "$name"}
            'Stop'   {Stop-Website -Name "$name" -ErrorAction SilentlyContinue}
            'Remove' {Remove-Website -Name "$name"}
        }
    }
}

<#
    .SYNOPSIS
        Delete the given IIS Application Pool. This is required to cleanup any
        existing conflicting apppools before setting up the endpoint.
#>

function Remove-AppPool
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $appPool
    )

    # Without this tests we may get a breaking error here, despite SilentlyContinue
    if (Test-Path -Path "IIS:\AppPools\$appPool")
    {
        Remove-WebAppPool -Name $appPool -ErrorAction SilentlyContinue
    }
}

<#
    .SYNOPSIS
        Generate an IIS Site Id while setting up the endpoint. The Site Id will
        be the max available in IIS config + 1.
#>

function New-SiteID
{
    [CmdletBinding()]
    param()

    return ((Get-Website | Foreach-Object -Process { $_.Id } | Measure-Object -Maximum).Maximum + 1)
}

<#
    .SYNOPSIS
        Copies the supplied PSWS config files to the IIS endpoint in inetpub
#>

function Copy-PSWSConfigurationToIISEndpointFolder
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $path,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $cfgfile,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $svc,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $mof,

        [Parameter()]
        [System.String]
        $dispatch,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [System.String]
        $asax,

        [Parameter()]
        [System.String[]]
        $dependentBinaries,

        [Parameter()]
        [System.String]
        $language,

        [Parameter()]
        [System.String[]]
        $dependentMUIFiles,

        [Parameter()]
        [System.String[]]
        $psFiles
    )

    if (!(Test-Path -Path $path))
    {
        $null = New-Item -ItemType container -Path $path
    }

    foreach ($dependentBinary in $dependentBinaries)
    {
        if (!(Test-Path -Path $dependentBinary))
        {
            throw "ERROR: $dependentBinary does not exist"
        }
    }

    Write-Verbose -Message 'Create the bin folder for deploying custom dependent binaries required by the endpoint'
    $binFolderPath = Join-Path -Path $path -ChildPath 'bin'
    $null = New-Item -Path $binFolderPath  -ItemType 'directory' -Force
    Copy-Item -Path $dependentBinaries -Destination $binFolderPath -Force

    foreach ($psFile in $psFiles)
    {
        if (!(Test-Path -Path $psFile))
        {
            throw "ERROR: $psFile does not exist"
        }

        Copy-Item -Path $psFile -Destination $path -Force
    }

    Copy-Item -Path $cfgfile (Join-Path -Path $path -ChildPath 'web.config') -Force
    Copy-Item -Path $svc -Destination $path -Force
    Copy-Item -Path $mof -Destination $path -Force

    if ($dispatch)
    {
        Copy-Item -Path $dispatch -Destination $path -Force
    }

    if ($asax)
    {
        Copy-Item -Path $asax -Destination $path -Force
    }
}

<#
    .SYNOPSIS
        Setup IIS Apppool, Site and Application
#>

function New-IISWebSite
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $site,

        [Parameter()]
        [System.String]
        $path,

        [Parameter()]
        [System.Int32]
        $port,

        [Parameter()]
        [System.String]
        $app,

        [Parameter()]
        [System.String]
        $appPool,

        [Parameter()]
        [System.String]
        $applicationPoolIdentityType,

        [Parameter()]
        [System.String]
        $certificateThumbPrint,

        [Parameter()]
        [System.Boolean]
        $enable32BitAppOnWin64
    )

    $siteID = New-SiteID

    Write-Verbose -Message 'Adding App Pool'
    $null = New-WebAppPool -Name $appPool

    Write-Verbose -Message 'Set App Pool Properties'
    $appPoolIdentity = 4
    if ($applicationPoolIdentityType)
    {
        # LocalSystem = 0, LocalService = 1, NetworkService = 2, SpecificUser = 3, ApplicationPoolIdentity = 4
        if ($applicationPoolIdentityType -eq 'LocalSystem')
        {
            $appPoolIdentity = 0
        }
        elseif ($applicationPoolIdentityType -eq 'LocalService')
        {
            $appPoolIdentity = 1
        }
        elseif ($applicationPoolIdentityType -eq 'NetworkService')
        {
            $appPoolIdentity = 2
        }
    }

    $appPoolItem = Get-Item IIS:\AppPools\$appPool
    $appPoolItem.managedRuntimeVersion = 'v4.0'
    $appPoolItem.enable32BitAppOnWin64 = $enable32BitAppOnWin64
    $appPoolItem.processModel.identityType = $appPoolIdentity
    $appPoolItem | Set-Item

    Write-Verbose -Message 'Add and Set Site Properties'
    if ($certificateThumbPrint -eq 'AllowUnencryptedTraffic')
    {
        New-WebSite -Name $site -Id $siteID -Port $port -IPAddress "*" -PhysicalPath $path -ApplicationPool $appPool | Out-Null
    }
    else
    {
        New-WebSite -Name $site -Id $siteID -Port $port -IPAddress "*" -PhysicalPath $path -ApplicationPool $appPool -Ssl | Out-Null

        # Remove existing binding for $port
        Remove-Item IIS:\SSLBindings\0.0.0.0!$port -ErrorAction Ignore

        # Create a new binding using the supplied certificate
        Get-Item CERT:\LocalMachine\MY\$certificateThumbPrint | New-Item IIS:\SSLBindings\0.0.0.0!$port | Out-Null
    }

    Update-Site -siteName $site -siteAction Start
}

<#
    .SYNOPSIS
        Allow Clients outsite the machine to access the setup endpoint on a
        User Port.
#>

function Set-FirewallConfigurationToAllowPullServerAccess
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $firewallPort
    )

    $script:netsh = "$env:windir\system32\netsh.exe"

    Write-Verbose -Message 'Disable Inbound Firewall Notification'
    & $script:netsh advfirewall set currentprofile settings inboundusernotification disable

    # remove all existing rules with that displayName
    & $script:netsh advfirewall firewall delete rule name=DSCPullServer_IIS_Port protocol=tcp localport=$firewallPort | Out-Null

    Write-Verbose -Message "Add Firewall Rule for port $firewallPort"
    & $script:netsh advfirewall firewall add rule name=DSCPullServer_IIS_Port dir=in action=allow protocol=TCP localport=$firewallPort
}

<#
    .SYNOPSIS
        Enable & Clear PSWS Operational/Analytic/Debug ETW Channels.
#>

function Enable-PSWSETW
{
    # Disable Analytic Log
    & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Analytic /e:false /q | Out-Null

    # Disable Debug Log
    & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Debug /e:false /q | Out-Null

    # Clear Operational Log
    & $script:wevtutil cl Microsoft-Windows-ManagementOdataService/Operational | Out-Null

    # Enable/Clear Analytic Log
    & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Analytic /e:true /q | Out-Null

    # Enable/Clear Debug Log
    & $script:wevtutil sl Microsoft-Windows-ManagementOdataService/Debug /e:true /q | Out-Null
}

<#
    .SYNOPSIS
        Create PowerShell WebServices IIS Endpoint
 
    .DESCRIPTION
        Creates a PSWS IIS Endpoint by consuming PSWS Schema and related
        dependent files
 
    .EXAMPLE
        New PSWS Endpoint [@ http://Server:39689/PSWS_Win32Process] by
        consuming PSWS Schema Files and any dependent scripts/binaries:
 
        New-PSWSEndpoint
            -site Win32Process
            -path $env:SystemDrive\inetpub\PSWS_Win32Process
            -cfgfile Win32Process.config
            -port 39689
            -app Win32Process
            -svc PSWS.svc
            -mof Win32Process.mof
            -dispatch Win32Process.xml
            -dependentBinaries ConfigureProcess.ps1, Rbac.dll
            -psFiles Win32Process.psm1
#>

function New-PSWSEndpoint
{
    [CmdletBinding()]
    param
    (
        # Unique Name of the IIS Site
        [Parameter()]
        [System.String]
        $site = 'PSWS',

        # Physical path for the IIS Endpoint on the machine (under inetpub)
        [Parameter()]
        [System.String]
        $path = "$env:SystemDrive\inetpub\PSWS",

        # Web.config file
        [Parameter()]
        [System.String]
        $cfgfile = 'web.config',

        # Port # for the IIS Endpoint
        [Parameter()]
        [System.Int32]
        $port = 8080,

        # IIS Application Name for the Site
        [Parameter()]
        [System.String]
        $app = 'PSWS',

        # IIS App Pool Identity Type - must be one of LocalService, LocalSystem, NetworkService, ApplicationPoolIdentity
        [Parameter()]
        [ValidateSet('LocalService', 'LocalSystem', 'NetworkService', 'ApplicationPoolIdentity')]
        [System.String]
        $applicationPoolIdentityType,

        # WCF Service SVC file
        [Parameter()]
        [System.String]
        $svc = 'PSWS.svc',

        # PSWS Specific MOF Schema File
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $mof,

        # PSWS Specific Dispatch Mapping File [Optional]
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $dispatch,

        # Global.asax file [Optional]
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $asax,

        # Any dependent binaries that need to be deployed to the IIS endpoint, in the bin folder
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $dependentBinaries,

         # MUI Language [Optional]
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $language,

        # Any dependent binaries that need to be deployed to the IIS endpoint, in the bin\mui folder [Optional]
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $dependentMUIFiles,

        # Any dependent PowerShell Scipts/Modules that need to be deployed to the IIS endpoint application root
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $psFiles,

        # True to remove all files for the site at first, false otherwise
        [Parameter()]
        [System.Boolean]
        $removeSiteFiles = $false,

        # Enable Firewall Exception for the supplied port
        [Parameter()]
        [System.Boolean]
        $EnableFirewallException,

        # Enable and Clear PSWS ETW
        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $EnablePSWSETW,

        # Thumbprint of the Certificate in CERT:\LocalMachine\MY\ for Pull Server
        [Parameter()]
        [System.String]
        $certificateThumbPrint = 'AllowUnencryptedTraffic',

        # When this property is set to true, Pull Server will run on a 32 bit process on a 64 bit machine
        [Parameter()]
        [System.Boolean]
        $Enable32BitAppOnWin64 = $false
    )

    $script:wevtutil = "$env:windir\system32\Wevtutil.exe"

    $svcName = Split-Path $svc -Leaf
    $protocol = 'https:'
    if ($certificateThumbPrint -eq 'AllowUnencryptedTraffic')
    {
        $protocol = 'http:'
    }

    # Get Machine Name
    $cimInstance = Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false

    Write-Verbose ("Setting up endpoint at - $protocol//" + $cimInstance.Name + ':' + $port + '/' + $svcName)
    Initialize-Endpoint -site $site -path $path -cfgfile $cfgfile -port $port -app $app `
                        -applicationPoolIdentityType $applicationPoolIdentityType -svc $svc -mof $mof `
                        -dispatch $dispatch -asax $asax -dependentBinaries $dependentBinaries `
                        -language $language -dependentMUIFiles $dependentMUIFiles -psFiles $psFiles `
                        -removeSiteFiles $removeSiteFiles -certificateThumbPrint $certificateThumbPrint `
                        -enable32BitAppOnWin64 $Enable32BitAppOnWin64

    if ($EnableFirewallException -eq $true)
    {
        Write-Verbose -Message "Enabling firewall exception for port $port"
        $null = Set-FirewallConfigurationToAllowPullServerAccess $port
    }

    if ($EnablePSWSETW)
    {
        Enable-PSWSETW
    }
}

<#
    .SYNOPSIS
        Removes a DSC WebServices IIS Endpoint
 
    .DESCRIPTION
        Removes a PSWS IIS Endpoint
 
    .EXAMPLE
        Remove the endpoint with the specified name:
 
        Remove-PSWSEndpoint -siteName PSDSCPullServer
#>

function Remove-PSWSEndpoint
{
    [CmdletBinding()]
    param
    (
        # Unique Name of the IIS Site
        [Parameter()]
        [System.String]
        $siteName
    )

    # Get the site to remove
    $site = Get-Item -Path "IIS:\sites\$siteName"
    # And the pool it is using
    $pool = $site.applicationPool

    # Get the path so we can delete the files
    $filePath = $site.PhysicalPath
    # Get the port number for the Firewall rule
    $bindings = (Get-WebBinding -Name $siteName).bindingInformation
    $port = [System.Text.RegularExpressions.Regex]::Match($bindings,':(\d+):').Groups[1].Value

    # Remove the actual site.
    Remove-Website -Name $siteName
    <#
      There may be running requests, wait a little
      I had an issue where the files were still in use
      when I tried to delete them
    #>

    Start-Sleep -Milliseconds 200

    # Remove the files for the site
    If (Test-Path -Path $filePath)
    {
        Get-ChildItem -Path $filePath -Recurse | Remove-Item -Recurse
        Remove-Item -Path $filePath
    }

    # Find out whether any other site is using this pool
    $filter = "/system.applicationHost/sites/site/application[@applicationPool='" + $pool + "']"
    $apps = (Get-WebConfigurationProperty -Filter $filter -PSPath 'machine/webroot/apphost' -name path).ItemXPath
    if (-not $apps -or $apps.count -eq 1)
    {
       # If we are the only site in the pool, remove the pool as well.
       Remove-WebAppPool -Name $pool
    }

    # Remove all rules with that name
    $ruleName = ($($FireWallRuleDisplayName) -f $port)
    Get-NetFirewallRule | Where-Object DisplayName -eq "$ruleName" | Remove-NetFirewallRule
}

<#
    .SYNOPSIS
        Set the option into the web.config for an endpoint
 
    .DESCRIPTION
        Set the options into the web.config for an endpoint allowing
        customization.
#>

function Set-AppSettingsInWebconfig
{
    [CmdletBinding()]
    param
    (
        # Physical path for the IIS Endpoint on the machine (possibly under inetpub)
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $path,

        # Key to add/update
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $key,

        # Value
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $value
    )

    $webconfig = Join-Path $path 'web.config'
    [System.Boolean] $Found = $false

    if (Test-Path -Path $webconfig)
    {
        $xml = [System.Xml.XmlDocument] (Get-Content -Path $webconfig)
        $root = $xml.get_DocumentElement()

        foreach( $item in $root.appSettings.add)
        {
            if( $item.key -eq $key )
            {
                $item.value = $value;
                $Found = $true;
            }
        }

        if( -not $Found)
        {
            $newElement = $xml.CreateElement('add')
            $nameAtt1 = $xml.CreateAttribute('key')
            $nameAtt1.psbase.value = $key;
            $null = $newElement.SetAttributeNode($nameAtt1)

            $nameAtt2 = $xml.CreateAttribute('value')
            $nameAtt2.psbase.value = $value;
            $null = $newElement.SetAttributeNode($nameAtt2)

            $null = $xml.configuration['appSettings'].AppendChild($newElement)
        }
    }

    $xml.Save($webconfig)
}

<#
    .SYNOPSIS
        Set the binding redirect setting in the web.config to redirect 10.0.0.0
        version of microsoft.isam.esent.interop to 6.3.0.0.
 
    .DESCRIPTION
        This function creates the following section in the web.config:
        <runtime>
          <assemblyBinding xmlns='urn:schemas-microsoft-com:asm.v1'>
            <dependentAssembly>
              <assemblyIdentity name='microsoft.isam.esent.interop' publicKeyToken='31bf3856ad364e35' />
            <bindingRedirect oldVersion='10.0.0.0' newVersion='6.3.0.0' />
           </dependentAssembly>
          </assemblyBinding>
        </runtime>
#>

function Set-BindingRedirectSettingInWebConfig
{
    [CmdletBinding()]
    param
    (
        # Physical path for the IIS Endpoint on the machine (possibly under inetpub)
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $path,

        # old version of the assembly
        [Parameter()]
        [System.String]
        $oldVersion = '10.0.0.0',

        # new version to redirect to
        [Parameter()]
        [System.String]
        $newVersion = '6.3.0.0'
    )

    $webconfig = Join-Path $path 'web.config'

    if (Test-Path -Path $webconfig)
    {
        $xml = [System.Xml.XmlDocument] (Get-Content -Path $webconfig)

        if(-not($xml.get_DocumentElement().runtime))
        {
            # Create the <runtime> section
            $runtimeSetting = $xml.CreateElement('runtime')

            # Create the <assemblyBinding> section
            $assemblyBindingSetting = $xml.CreateElement('assemblyBinding')
            $xmlnsAttribute = $xml.CreateAttribute('xmlns')
            $xmlnsAttribute.Value = 'urn:schemas-microsoft-com:asm.v1'
            $assemblyBindingSetting.Attributes.Append($xmlnsAttribute)

            # The <assemblyBinding> section goes inside <runtime>
            $null = $runtimeSetting.AppendChild($assemblyBindingSetting)

            # Create the <dependentAssembly> section
            $dependentAssemblySetting = $xml.CreateElement('dependentAssembly')

            # The <dependentAssembly> section goes inside <assemblyBinding>
            $null = $assemblyBindingSetting.AppendChild($dependentAssemblySetting)

            # Create the <assemblyIdentity> section
            $assemblyIdentitySetting = $xml.CreateElement('assemblyIdentity')
            $nameAttribute = $xml.CreateAttribute('name')
            $nameAttribute.Value = 'microsoft.isam.esent.interop'
            $publicKeyTokenAttribute = $xml.CreateAttribute('publicKeyToken')
            $publicKeyTokenAttribute.Value = '31bf3856ad364e35'
            $null = $assemblyIdentitySetting.Attributes.Append($nameAttribute)
            $null = $assemblyIdentitySetting.Attributes.Append($publicKeyTokenAttribute)

            # <assemblyIdentity> section goes inside <dependentAssembly>
            $dependentAssemblySetting.AppendChild($assemblyIdentitySetting)

            # Create the <bindingRedirect> section
            $bindingRedirectSetting = $xml.CreateElement('bindingRedirect')
            $oldVersionAttribute = $xml.CreateAttribute('oldVersion')
            $newVersionAttribute = $xml.CreateAttribute('newVersion')
            $oldVersionAttribute.Value = $oldVersion
            $newVersionAttribute.Value = $newVersion
            $null = $bindingRedirectSetting.Attributes.Append($oldVersionAttribute)
            $null = $bindingRedirectSetting.Attributes.Append($newVersionAttribute)

            # The <bindingRedirect> section goes inside <dependentAssembly> section
            $dependentAssemblySetting.AppendChild($bindingRedirectSetting)

            # The <runtime> section goes inside <Configuration> section
            $xml.configuration.AppendChild($runtimeSetting)

            $xml.Save($webconfig)
        }
    }
}

Export-ModuleMember -function New-PSWSEndpoint, Set-AppSettingsInWebconfig, Set-BindingRedirectSettingInWebConfig, Remove-PSWSEndpoint