DSCResources/DSC_WSManListener/DSC_WSManListener.psm1

$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules'

Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common')

# Import Localization Strings
$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'


# Standard Transport Ports
$Default_HTTP_Port = 5985
$Default_HTTPS_Port = 5986

<#
    .SYNOPSIS
        Returns the current WS-Man Listener details.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Ensure
        Specifies whether the WS-Man Listener should exist.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP', 'HTTPS')]
        [System.String]
        $Transport,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure
    )

    Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($script:localizedData.GettingListenerMessage)
        ) -join '' )

    $returnValue = @{
        Transport = $Transport
    }

    # Lookup the existing Listener
    $listener = Get-Listener -Transport $Transport

    if ($listener)
    {
        $certificate = ''
        
        # An existing listener matching the transport was found
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.ListenerExistsMessage) `
                    -f $Transport
            ) -join '' )

        if ($listener.CertificateThumbprint)
        {
            $certificate = Find-Certificate -CertificateThumbprint $listener.CertificateThumbprint
        }

        $returnValue += @{
            Ensure                = 'Present'
            Port                  = $listener.Port
            Address               = $listener.Address
            Issuer                = $certificate.Issuer
            SubjectFormat         = $null
            MatchAlternate        = $null
            DN                    = $null
            Hostname              = $listener.Hostname
            Enabled               = $listener.Enabled
            URLPrefix             = $listener.URLPrefix
            CertificateThumbprint = $listener.CertificateThumbprint
        }
    }
    else
    {
        # An existing listener matching the transport was not found
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.ListenerDoesNotExistMessage) `
                    -f $Transport
            ) -join '' )

        $returnValue += @{
            Ensure                = 'Absent'
            Port                  = $null
            Address               = $null
            Issuer                = $null
            SubjectFormat         = $null
            MatchAlternate        = $null
            DN                    = $null
            Hostname              = $null
            Enabled               = $null
            URLPrefix             = $null
            CertificateThumbprint = $null
        }
    } # if

    return $returnValue
} # Get-TargetResource

<#
    .SYNOPSIS
    Sets the state of a WS-Man Listener.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Ensure
        Specifies whether the WS-Man Listener should exist.
 
    .PARAMETER Port
        The port the WS-Man Listener should use. Defaults to 5985 for HTTP and 5986 for HTTPS listeners.
 
    .PARAMETER Address
        The Address that the WS-Man Listener will be bound to. The default is * (any address).
 
    .PARAMETER Issuer
        The Issuer of the certificate to use for the HTTPS WS-Man Listener if a thumbprint is
        not specified.
 
    .PARAMETER SubjectFormat
        The format used to match the certificate subject to use for an HTTPS WS-Man Listener
        if a thumbprint is not specified.
 
    .PARAMETER MatchAlternate
        Should the FQDN/Name be used to also match the certificate alternate subject for an HTTPS WS-Man
        Listener if a thumbprint is not specified.
 
    .PARAMETER DN
        This is a Distinguished Name component that will be used to identify the certificate to use
        for the HTTPS WS-Man Listener if a thumbprint is not specified.
 
    .PARAMETER CertificateThumbprint
        The Thumbprint of the certificate to use for the HTTPS WS-Man Listener.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP', 'HTTPS')]
        [System.String]
        $Transport,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [System.UInt16]
        $Port,

        [Parameter()]
        [System.String]
        $Address = '*',

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

        [Parameter()]
        [ValidateSet('Both', 'FQDNOnly', 'NameOnly')]
        [System.String]
        $SubjectFormat = 'Both',

        [Parameter()]
        [System.Boolean]
        $MatchAlternate,

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

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

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

    Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($script:localizedData.SettingListenerMessage)
        ) -join '' )

    # Lookup the existing Listener
    $listener = Get-Listener -Transport $Transport

    # Get the default port for the transport if none was provided
    $Port = Get-DefaultPort -Transport $Transport -Port $Port

    if ($Ensure -eq 'Present')
    {
        # The listener should exist
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.EnsureListenerExistsMessage) `
                    -f $Transport, $Port
            ) -join '' )

        if ($listener)
        {
            # The Listener exists already - delete it
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerExistsRemoveMessage) `
                        -f $Transport, $Port
                ) -join '' )

            Remove-WSManInstance `
                -ResourceURI 'winrm/config/Listener' `
                -SelectorSet @{
                    Transport = $listener.Transport
                    Address   = $listener.Address
                }
        }
        else
        {
            # Ths listener doesn't exist - do nothing
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerOnPortDoesNotExistMessage) `
                        -f $Transport, $Port
                ) -join '' )
        }

        # Create the listener
        if ($Transport -eq 'HTTPS')
        {
            # Find the certificate to use for the HTTPS Listener
            $null = $PSBoundParameters.Remove('Transport')
            $null = $PSBoundParameters.Remove('Ensure')
            $null = $PSBoundParameters.Remove('Port')
            $null = $PSBoundParameters.Remove('Address')

            $certificate = Find-Certificate @PSBoundParameters
            [System.String] $thumbprint = $certificate.thumbprint

            if ($thumbprint)
            {
                # A certificate was found, so use it to enable the HTTPS WinRM listener
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.CreatingListenerMessage) `
                            -f $Transport, $Port
                    ) -join '' )

                if ([System.String]::IsNullOrEmpty($Hostname))
                {
                    $Hostname = [System.Net.Dns]::GetHostByName($ENV:computerName).Hostname
                }

                New-WSManInstance `
                    -ResourceURI 'winrm/config/Listener' `
                    -SelectorSet @{
                        Address   = $Address
                        Transport = $Transport
                    } `
                    -ValueSet @{
                        Hostname              = $Hostname
                        CertificateThumbprint = $thumbprint
                        Port                  = $Port
                    } `
                    -ErrorAction Stop
            }
            else
            {
                # A certificate could not be found to use for the HTTPS listener
                New-InvalidArgumentException `
                    -Message ($script:localizedData.ListenerCreateFailNoCertError -f `
                        $Transport, $Port) `
                    -Argument 'Issuer'
            } # if
        }
        else
        {
            # Create a plain HTTP listener
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.CreatingListenerMessage) `
                        -f $Transport, $Port
                ) -join '' )

            New-WSManInstance `
                -ResourceURI 'winrm/config/Listener' `
                -SelectorSet @{
                    Address   = $Address
                    Transport = $Transport
                } `
                -ValueSet @{
                    Port = $Port
                } `
                -ErrorAction Stop
        }
    }
    else
    {
        # The listener should not exist
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.EnsureListenerDoesNotExistMessage) `
                    -f $Transport, $Port
            ) -join '' )

        if ($listener)
        {
            # The listener does exist - so delete it
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerExistsRemoveMessage) `
                        -f $Transport, $Port
                ) -join '' )

            Remove-WSManInstance `
                -ResourceURI 'winrm/config/Listener' `
                -SelectorSet @{
                    Transport = $listener.Transport
                    Address   = $listener.Address
                }
        }
    } # if
} # Set-TargetResource

<#
    .SYNOPSIS
        Tests the state of a WS-Man Listener.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Ensure
        Specifies whether the WS-Man Listener should exist.
 
    .PARAMETER Port
        The port the WS-Man Listener should use. Defaults to 5985 for HTTP and 5986 for HTTPS listeners.
 
    .PARAMETER Address
        The Address that the WS-Man Listener will be bound to. The default is * (any address).
 
    .PARAMETER Issuer
        The Issuer of the certificate to use for the HTTPS WS-Man Listener if a thumbprint is
        not specified.
 
    .PARAMETER SubjectFormat
        The format used to match the certificate subject to use for an HTTPS WS-Man Listener
        if a thumbprint is not specified.
 
    .PARAMETER MatchAlternate
        Should the FQDN/Name be used to also match the certificate alternate subject for an HTTPS WS-Man
        Listener if a thumbprint is not specified.
 
    .PARAMETER DN
        This is a Distinguished Name component that will be used to identify the certificate to use
        for the HTTPS WS-Man Listener if a thumbprint is not specified.
 
    .PARAMETER CertificateThumbprint
        The Thumbprint of the certificate to use for the HTTPS WS-Man Listener.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP', 'HTTPS')]
        [System.String]
        $Transport,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure,

        [Parameter()]
        [System.UInt16]
        $Port,

        [Parameter()]
        [System.String]
        $Address = '*',

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

        [Parameter()]
        [ValidateSet('Both', 'FQDNOnly', 'NameOnly')]
        [System.String]
        $SubjectFormat = 'Both',

        [Parameter()]
        [System.Boolean]
        $MatchAlternate,

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

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

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

    # Flag to signal whether settings are correct
    $desiredConfigurationMatch = $true

    Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($script:localizedData.TestingListenerMessage)
        ) -join '' )

    # Lookup the existing Listener
    $listener = Get-Listener -Transport $Transport

    # Get the default port for the transport if none was provided
    $Port = Get-DefaultPort -Transport $Transport -Port $Port

    if ($Ensure -eq 'Present')
    {
        # The listener should exist
        if ($listener)
        {
            # The Listener exists already
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerExistsMessage) `
                        -f $Transport
                ) -join '' )

            # Check it is setup as per parameters
            if ($listener.Port -ne $Port)
            {
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.ListenerOnWrongPortMessage) `
                            -f $Transport, $listener.Port, $Port
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            if ($listener.Address -ne $Address)
            {
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.ListenerOnWrongAddressMessage) `
                            -f $Transport, $listener.Address, $Address
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            if ($PSBoundParameters.ContainsKey('Hostname') -and $listener.Hostname -ne $Hostname)
            {
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.ListenerOnWrongHostnameMessage) `
                            -f $Transport, $listener.Hostname, $Hostname
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            if ($PSBoundParameters.ContainsKey('CertificateThumbprint') -and $listener.CertificateThumbprint -ne $CertificateThumbprint)
            {
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.ListenerOnWrongCertificateThumbprintMessage) `
                            -f $Transport, $listener.CertificateThumbprint, $CertificateThumbprint
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }
        }
        else
        {
            # Ths listener doesn't exist but should
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerDoesNotExistButShouldMessage) `
                        -f $Transport
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
    }
    else
    {
        # The listener should not exist
        if ($listener)
        {
            # The listener exists but should not
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerExistsButShouldNotMessage) `
                        -f $Transport
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
        else
        {
            # The listener does not exist and should not
            Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($script:localizedData.ListenerDoesNotExistAndShouldNotMessage) `
                        -f $Transport
                ) -join '' )
        }
    } # if

    return $desiredConfigurationMatch
} # Test-TargetResource

<#
    .SYNOPSIS
        Looks up a WS-Man listener on the machine and returns the details.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
#>

function Get-Listener
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP', 'HTTPS')]
        [System.String]
        $Transport
    )

    $listeners = @(Get-WSManInstance `
            -ResourceURI 'winrm/config/Listener' `
            -Enumerate)

    if ($listeners)
    {
        return $listeners.Where( { ($_.Transport -eq $Transport) `
                    -and ($_.Source -ne 'Compatibility') } )
    }
} # Get-Listener

<#
    .SYNOPSIS
        Returns the port to use for the listener based on the transport and port.
 
    .PARAMETER Transport
        The transport type of WS-Man Listener.
 
    .PARAMETER Port
        The port the WS-Man Listener should use. Defaults to 5985 for HTTP and 5986 for HTTPS listeners.
#>

function Get-DefaultPort
{
    [CmdletBinding()]
    [OutputType([System.UInt16])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('HTTP', 'HTTPS')]
        [System.String]
        $Transport,

        [Parameter()]
        [System.UInt16]
        $Port
    )

    if (-not $Port)
    {
        # Set the default port because none was provided
        if ($Transport -eq 'HTTP')
        {
            $Port = $Default_HTTP_Port
        }
        else
        {
            $Port = $Default_HTTPS_Port
        }
    }
    return $Port
}

<#
    .SYNOPSIS
        Finds the certificate to use for the HTTPS WS-Man Listener
 
    .PARAMETER Issuer
        The Issuer of the certificate to use for the HTTPS WS-Man Listener if a thumbprint is
        not specified.
 
    .PARAMETER SubjectFormat
        The format used to match the certificate subject to use for an HTTPS WS-Man Listener
        if a thumbprint is not specified.
 
    .PARAMETER MatchAlternate
        Should the FQDN/Name be used to also match the certificate alternate subject for an HTTPS WS-Man
        Listener if a thumbprint is not specified.
 
    .PARAMETER DN
        This is a Distinguished Name component that will be used to identify the certificate to use
        for the HTTPS WS-Man Listener if a thumbprint is not specified.
 
    .PARAMETER CertificateThumbprint
        The Thumbprint of the certificate to use for the HTTPS WS-Man Listener.
#>

function Find-Certificate
{
    [CmdletBinding()]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    param
    (
        [Parameter()]
        [System.String]
        $Issuer,

        [Parameter()]
        [ValidateSet('Both', 'FQDNOnly', 'NameOnly')]
        [System.String]
        $SubjectFormat = 'Both',

        [Parameter()]
        [System.Boolean]
        $MatchAlternate,

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

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

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

    if ($PSBoundParameters.ContainsKey('CertificateThumbprint'))
    {
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.FindCertificateByThumbprintMessage) `
                    -f $CertificateThumbprint
            ) -join '' )

        $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                ($_.Thumbprint -eq $CertificateThumbprint)
        } | Select-Object -First 1
    }
    else
    {
        # First try and find a certificate that is used to the FQDN of the machine
        if ($SubjectFormat -in 'Both', 'FQDNOnly')
        {
            # Lookup the certificate using the FQDN of the machine
            if ([System.String]::IsNullOrEmpty($Hostname))
            {
                $Hostname = [System.Net.Dns]::GetHostByName($ENV:computerName).Hostname
            }
            $Subject = "CN=$Hostname"

            if ($PSBoundParameters.ContainsKey('DN'))
            {
                $Subject = "$Subject, $DN"
            } # if

            if ($MatchAlternate)
            {
                # Try and lookup the certificate using the subject and the alternate name
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.FindCertificateAlternateMessage) `
                            -f $Subject, $Issuer, $Hostname
                    ) -join '' )

                $certificate = (Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName `
                            -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($Hostname -in $_.DNSNameList.Unicode) -and
                        ($_.Subject -eq $Subject)
                    } | Select-Object -First 1)
            }
            else
            {
                # Try and lookup the certificate using the subject name
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.FindCertificateMessage) `
                            -f $Subject, $Issuer
                    ) -join '' )

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName `
                        -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($_.Subject -eq $Subject)
                } | Select-Object -First 1
            } # if
        }

        if (-not $certificate `
                -and ($SubjectFormat -in 'Both', 'NameOnly'))
        {
            # If could not find an FQDN cert, try for one issued to the computer name
            [System.String] $Hostname = $ENV:ComputerName
            [System.String] $Subject = "CN=$Hostname"

            if ($PSBoundParameters.ContainsKey('DN'))
            {
                $Subject = "$Subject, $DN"
            } # if

            if ($MatchAlternate)
            {
                # Try and lookup the certificate using the subject and the alternate name
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.FindCertificateAlternateMessage) `
                            -f $Subject, $Issuer, $Hostname
                    ) -join '' )

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName `
                        -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($Hostname -in $_.DNSNameList.Unicode) -and
                        ($_.Subject -eq $Subject)
                } | Select-Object -First 1
            }
            else
            {
                # Try and lookup the certificate using the subject name
                Write-Verbose -Message ( @(
                        "$($MyInvocation.MyCommand): "
                        $($script:localizedData.FindCertificateMessage) `
                            -f $Subject, $Issuer
                    ) -join '' )

                $certificate = Get-ChildItem -Path Cert:\localmachine\my | Where-Object -FilterScript {
                        ($_.Extensions.EnhancedKeyUsages.FriendlyName `
                        -contains 'Server Authentication') -and
                        ($_.Issuer -eq $Issuer) -and
                        ($_.Subject -eq $Subject)
                } | Select-Object -First 1
            } # if
        } # if
    } # if

    if ($certificate)
    {
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.CertificateFoundMessage) `
                    -f $certificate.thumbprint
            ) -join '' )
    }
    else
    {
        Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($script:localizedData.CertificateNotFoundMessage) `
            ) -join '' )
    } # if

    return $certificate
} # Find-Certificate

Export-ModuleMember -Function *-TargetResource