PSModules/Carbon.Windows.HttpServer/1.0.0/Carbon.Windows.HttpServer.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $script:moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$script:moduleRoot = $PSScriptRoot

Add-Type -TypeDefinition (Get-Content -Raw -Path (Join-Path -Path $script:moduleRoot -ChildPath 'src\Http.cs' -Resolve))

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Get-CSslCertificateBinding
{
    <#
    .SYNOPSIS
    Gets the SSL certificate bindings on this computer.
 
    .DESCRIPTION
    Windows binds SSL certificates to an IP addresses/port combination. This function gets all the SSL bindings on this computer, or a binding for a specific IP/port, or $null if one doesn't exist. The bindings are returned as `Carbon.Windows.HttpServer.SslCertificateBinding` objects.
 
    .OUTPUTS
    Carbon.Windows.HttpServer.SslCertificateBinding.
 
    .EXAMPLE
    > Get-CSslCertificateBinding
 
    Gets all the SSL certificate bindings on the local computer.
 
    .EXAMPLE
    > Get-CSslCertificateBinding -IPAddress 42.37.80.47 -Port 443
 
    Gets the SSL certificate bound to 42.37.80.47, port 443.
 
    .EXAMPLE
    > Get-CSslCertificateBinding -Port 443
 
    Gets the default SSL certificate bound to ALL the computer's IP addresses on port 443.
    #>

    [CmdletBinding()]
    [OutputType([Carbon.Windows.HttpServer.SslCertificateBinding])]
    param(
        # The IP address whose certificate(s) to get. Should be in the form IP:port. Optional.
        [ipaddress] $IPAddress,

        # The port whose certificate(s) to get. Optional.
        [UInt16] $Port
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    [Carbon.Windows.HttpServer.SslCertificateBinding]::GetSslCertificateBindings() |
        Where-Object {
            if( $IPAddress )
            {
                return $_.IPAddress -eq $IPAddress
            }
            return $true
        } |
        Where-Object {
            if( $Port )
            {
                return $_.Port -eq $Port
            }
            return $true
        }

}



function Invoke-Netsh
{
    <#
    .SYNOPSIS
    INTERNAL.
 
    .DESCRIPTION
    INTERNAL.
 
    .EXAMPLE
    INTERNAL.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The target of the action.
        [Parameter(Mandatory)]
        [String] $Target,

        # The action/command being performed.
        [Parameter(Mandatory)]
        [String] $Action,

        # The command to run.
        [Parameter(Mandatory, ValueFromRemainingArguments, Position=0)]
        [String[]] $ArgumentList
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if (-not $PSCmdlet.ShouldProcess($Target, $Action))
    {
        return
    }

    Write-Information "netsh $($ArgumentList -join ' ')"
    $output = netsh $ArgumentList
    if( $LASTEXITCODE )
    {
        $output = $output -join [Environment]::NewLine
        $msg = "Netsh command ""$($Action)"" on ""$($Target)"" exited with code $($LASTEXITCODE): $($output)"
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    $output | Where-Object { $null -ne $_ } | Write-Verbose
}



function Remove-CSslCertificateBinding
{
    <#
    .SYNOPSIS
    Removes an SSL certificate binding.
 
    .DESCRIPTION
    Uses the netsh command line application to remove an SSL certificate binding for an IP/port combination. If the binding doesn't exist, nothing is changed.
 
    .EXAMPLE
    > Remove-CSslCertificateBinding -IPAddress '45.72.89.57' -Port 443
 
    Removes the SSL certificate bound to IP 45.72.89.57 on port 443.
 
    .EXAMPLE
    > Remove-CSslCertificateBinding
 
    Removes the default SSL certificate from port 443. The default certificate is bound to all IP addresses.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The IP address whose binding to remove. Default is all IP addresses.
        [ipaddress] $IPAddress = '0.0.0.0',

        # The port of the binding to remove. Default is port 443.
        [UInt16] $Port = 443
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if (-not (Test-CSslCertificateBinding -IPAddress $IPAddress -Port $Port))
    {
        return
    }

    if( $IPAddress.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetworkV6 )
    {
        $ipPort = '[{0}]:{1}' -f $IPAddress,$Port
    }
    else
    {
        $ipPort = '{0}:{1}' -f $IPAddress,$Port
    }

    Invoke-Netsh http delete sslcert ipPort=$ipPort `
                 -Target $ipPort `
                 -Action "removing SSL certificate binding"
}




function Set-CSslCertificateBinding
{
    <#
    .SYNOPSIS
    Sets an SSL certificate binding for a given IP/port.
 
    .DESCRIPTION
    Uses the netsh command line application to set the certificate for an IP address and port. If a binding already exists for the IP/port, it is removed, and the new binding is created.
 
    Beginning with Carbon 2.0, returns a `Carbon.Windows.HttpServer.SslCertificateBinding` object for the binding that was set.
 
    .OUTPUTS
    Carbon.Windows.HttpServer.SslCertificateBinding.
 
    .EXAMPLE
    Set-CSslCertificateBinding -IPAddress 43.27.89.54 -Port 443 -ApplicationID 88d1f8da-aeb5-40a2-a5e5-0e6107825df7 -Thumbprint 4789073458907345907434789073458907345907
 
    Configures the computer to use the 478907345890734590743 certificate on IP 43.27.89.54, port 443.
 
    .EXAMPLE
    Set-CSslCertificateBinding -ApplicationID 88d1f8da-aeb5-40a2-a5e5-0e6107825df7 -Thumbprint 4789073458907345907434789073458907345907
 
    Configures the compute to use the 478907345890734590743 certificate as the default certificate on all IP addresses, port 443.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([Carbon.Windows.HttpServer.SslCertificateBinding])]
    param(
        [ipaddress]
        # The IP address for the binding. Defaults to all IP addresses.
        $IPAddress = '0.0.0.0',

        [UInt16]
        # The port for the binding. Defaults to 443.
        $Port = 443,

        [Parameter(Mandatory)]
        [Guid]
        # A unique ID representing the application using the binding. Create your own.
        $ApplicationID,

        [Parameter(Mandatory)]
        [ValidatePattern("^[0-9a-f]{40}$")]
        [String]
        # The thumbprint of the certificate to use. The certificate must be installed.
        $Thumbprint,

        [switch]
        # Return a `Carbon.Windows.HttpServer.SslCertificateBinding` for the configured binding.
        $PassThru
    )

    Set-StrictMode -Version 'Latest'

    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if( $IPAddress.AddressFamily -eq [Net.Sockets.AddressFamily]::InterNetworkV6 )
    {
        $ipPort = '[{0}]:{1}' -f $IPAddress,$Port
    }
    else
    {
        $ipPort = '{0}:{1}' -f $IPAddress,$Port
    }

    Remove-CSslCertificateBinding -IPAddress $IPAddress -Port $Port

    $action = 'creating SSL certificate binding'
    if( $PSCmdlet.ShouldProcess( $IPPort, $action ) )
    {
        $appID = $ApplicationID.ToString('B')
        Invoke-Netsh http add sslcert ipport=$ipPort certhash=$Thumbprint appid=$appID `
                     -Target $ipPort `
                     -Action $action

        if( $PassThru )
        {
            Get-CSslCertificateBinding -IPAddress $IPAddress -Port $Port
        }
    }
}




function Test-CSslCertificateBinding
{
    <#
    .SYNOPSIS
    Tests if an SSL certificate binding exists.
 
    .DESCRIPTION
    SSL certificates are bound to IP addresses and ports. This function tests if one exists on a given IP address/port.
 
    .EXAMPLE
    Test-CSslCertificateBinding -Port 443
 
    Tests if there is a default SSL certificate bound to all a machine's IP addresses on port 443.
 
    .EXAMPLE
    Test-CSslCertificateBinding -IPAddress 10.0.1.1 -Port 443
 
    Tests if there is an SSL certificate bound to IP address 10.0.1.1 on port 443.
 
    .EXAMPLE
    Test-CSslCertificateBinding
 
    Tests if there are any SSL certificates bound to any IP address/port on the machine.
    #>

    [CmdletBinding()]
    param(
        [ipaddress]
        # The IP address to test for an SSL certificate.
        $IPAddress,

        [Uint16]
        # The port to test for an SSL certificate.
        $Port
    )

    Set-StrictMode -Version 'Latest'

    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{ }
    if( $IPAddress )
    {
        $getArgs.IPAddress = $IPAddress
    }

    if( $Port )
    {
        $getArgs.Port = $Port
    }

    $binding = Get-CSslCertificateBinding @getArgs
    if( $binding )
    {
        return $True
    }
    else
    {
        return $False
    }
}




function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}